Orchestration / payments v2 #554

Merged
tech merged 23 commits from pqpov2-547 into main 2026-02-26 22:45:55 +00:00
499 changed files with 30817 additions and 21315 deletions

View File

@@ -14,6 +14,7 @@ when:
path:
include:
- api/gateway/chain/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
ignore_message: '[rebuild]'

View File

@@ -13,6 +13,7 @@ when:
path:
include:
- api/gateway/mntx/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
ignore_message: '[rebuild]'

View File

@@ -11,6 +11,7 @@ when:
path:
include:
- api/gateway/tgsettle/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
ignore_message: '[rebuild]'

View File

@@ -14,6 +14,7 @@ when:
path:
include:
- api/gateway/tron/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
ignore_message: '[rebuild]'

View File

@@ -85,7 +85,7 @@ init:
@echo "$(GREEN)Verifying .env.dev...$(NC)"
@cat .env.dev | grep -q "MONGO_USER=" || (echo "$(YELLOW)Error: .env.dev is incomplete$(NC)" && exit 1)
@echo "$(GREEN)Running proto generation...$(NC)"
@./generate_protos.sh
@./ci/scripts/proto/generate.sh
@echo "$(GREEN)Building Docker images...$(NC)"
@$(COMPOSE) build
@echo "$(GREEN)✅ Initialization complete!$(NC)"
@@ -97,7 +97,7 @@ init:
# Build all images
build:
@echo "$(GREEN)Building all service images...$(NC)"
@./generate_protos.sh
@./ci/scripts/proto/generate.sh
@$(COMPOSE) build
# Start all services
@@ -154,7 +154,7 @@ generate: generate-api generate-frontend
# Generate protobuf code
generate-api:
@echo "$(GREEN)Generating protobuf code...$(NC)"
@./generate_protos.sh
@./ci/scripts/proto/generate.sh
@echo "$(GREEN)✅ Protobuf generation complete$(NC)"
# Generate Flutter code (json_serializable, etc.)

View File

@@ -5,10 +5,10 @@ go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
require (
github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.9
github.com/aws/aws-sdk-go-v2/credentials v1.19.9
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
github.com/aws/aws-sdk-go-v2 v1.41.2
github.com/aws/aws-sdk-go-v2/config v1.32.10
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
github.com/jung-kurt/gofpdf v1.16.2
github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0
@@ -20,21 +20,21 @@ require (
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/smithy-go v1.24.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
@@ -48,12 +48,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -61,10 +61,10 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -4,44 +4,44 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 h1:+VTRawC4iVY58pS/lzpo0lnoa/SYNGF4/B/3/U5ro8Y=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
@@ -134,8 +134,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -158,8 +158,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
@@ -229,8 +229,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -258,8 +258,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -392,7 +392,7 @@ func (s *Service) startDiscoveryAnnouncer() {
announce := discovery.Announcement{
Service: "BILLING_DOCUMENTS",
Operations: []string{"documents.batch_resolve", "documents.get"},
Operations: []string{discovery.OperationDocumentsBatchResolve, discovery.OperationDocumentsGet},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}

View File

@@ -32,13 +32,13 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -46,10 +46,10 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/protobuf v1.36.11
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/billing/fees/storage"
"github.com/tech/sendico/billing/fees/storage/model"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
@@ -22,10 +23,10 @@ type planFinder interface {
type feeResolver struct {
plans storage.PlansStore
finder planFinder
logger *zap.Logger
logger mlogger.Logger
}
func New(plans storage.PlansStore, logger *zap.Logger) *feeResolver {
func New(plans storage.PlansStore, logger mlogger.Logger) *feeResolver {
var finder planFinder
if pf, ok := plans.(planFinder); ok {
finder = pf

View File

@@ -564,7 +564,7 @@ func (s *Service) startDiscoveryAnnouncer() {
announce := discovery.Announcement{
Service: "BILLING_FEES",
Operations: []string{"fee.calc"},
Operations: []string{discovery.OperationFeeCalc},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}

View File

@@ -25,12 +25,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -39,11 +39,11 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -51,7 +51,7 @@ func (i *Imp) startDiscovery(cfg *config) error {
announce := discovery.Announcement{
Service: "DISCOVERY",
InstanceID: discovery.InstanceID(),
Operations: []string{"discovery.lookup"},
Operations: []string{discovery.OperationDiscoveryLookup},
Version: appversion.Create().Short(),
}
i.announcer = discovery.NewAnnouncer(i.logger, producer, mservice.Discovery, announce)

View File

@@ -13,7 +13,7 @@ require (
github.com/tech/sendico/fx/storage v0.0.0
github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1
golang.org/x/net v0.50.0
golang.org/x/net v0.51.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -30,12 +30,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -47,7 +47,7 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -86,7 +86,7 @@ func (a *App) Run(ctx context.Context) error {
producer := msgproducer.NewProducer(a.logger.Named("discovery_producer"), broker)
announce := discovery.Announcement{
Service: "FX_INGESTOR",
Operations: []string{"fx.ingest"},
Operations: []string{discovery.OperationFXIngest},
Version: appversion.Create().Short(),
}
announcer = discovery.NewAnnouncer(a.logger, producer, "fx_ingestor", announce)

View File

@@ -415,7 +415,7 @@ type valuteMapping struct {
byID map[string]valuteInfo
}
func buildValuteMapping(logger *zap.Logger, items []valuteItem) (*valuteMapping, error) { //nolint:cyclop,gocognit,nestif
func buildValuteMapping(logger mlogger.Logger, items []valuteItem) (*valuteMapping, error) { //nolint:cyclop,gocognit,nestif
byISO := make(map[string]valuteInfo, len(items))
byID := make(map[string]valuteInfo, len(items))
byNum := make(map[string]string, len(items))

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"strings"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
@@ -17,7 +18,7 @@ const (
type httpClient struct {
client *http.Client
headers http.Header
logger *zap.Logger
logger mlogger.Logger
}
type httpClientOptions struct {
@@ -26,7 +27,7 @@ type httpClientOptions struct {
referer string
}
func newHTTPClient(logger *zap.Logger, client *http.Client, opts httpClientOptions) *httpClient {
func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOptions) *httpClient {
userAgent := opts.userAgent
if strings.TrimSpace(userAgent) == "" {
userAgent = defaultUserAgent

View File

@@ -31,12 +31,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -44,9 +44,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -106,7 +106,7 @@ func (s *Service) startDiscoveryAnnouncer() {
}
announce := discovery.Announcement{
Service: "FX_ORACLE",
Operations: []string{"fx.quote"},
Operations: []string{discovery.OperationFXQuote},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}

View File

@@ -7,7 +7,7 @@ replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/gateway/common => ../common
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1
github.com/ethereum/go-ethereum v1.17.0
github.com/hashicorp/vault/api v1.22.0
github.com/mitchellh/mapstructure v1.5.0
@@ -25,7 +25,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260218040609-6f1c0c95351b // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
@@ -38,7 +38,7 @@ require (
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -61,13 +61,13 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/supranational/blst v0.3.16 // indirect
@@ -86,10 +86,10 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260218040609-6f1c0c95351b h1:RVnS+OZmBJbbNeqejAksq3Mxc73y0IEzyTUHPPWZuj8=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260218040609-6f1c0c95351b/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc h1:1stW1OipdBj8Me+nj26SzT8yil7OYve0r3cWobzk1JQ=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -60,8 +60,8 @@ github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+Zlfu
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
@@ -72,8 +72,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes=
@@ -204,8 +204,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -236,8 +236,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
@@ -322,8 +322,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -355,8 +355,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -224,8 +224,8 @@ func (s *Service) startDiscoveryAnnouncers() {
announce := discovery.Announcement{
ID: discovery.StableCryptoRailGatewayID(string(network.Name)),
Service: "CRYPTO_RAIL_GATEWAY",
Rail: "CRYPTO",
Operations: []string{"balance.read", "payin.crypto", "payout.crypto", "fee.send", "observe.confirm"},
Rail: discovery.RailCrypto,
Operations: discovery.CryptoRailGatewayOperations(),
Currencies: currencies,
InvokeURI: s.invokeURI,
Version: version,

View File

@@ -14,7 +14,7 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect

View File

@@ -60,8 +60,8 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=

View File

@@ -7,6 +7,7 @@ import (
"github.com/shopspring/decimal"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model/account_role"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
@@ -35,7 +36,7 @@ type gatewayClient struct {
conn *grpc.ClientConn
client grpcConnectorClient
cfg Config
logger *zap.Logger
logger mlogger.Logger
}
// New dials the Monetix gateway.

View File

@@ -3,6 +3,7 @@ package client
import (
"time"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
@@ -11,7 +12,7 @@ type Config struct {
Address string
DialTimeout time.Duration
CallTimeout time.Duration
Logger *zap.Logger
Logger mlogger.Logger
}
func (c *Config) setDefaults() {

View File

@@ -35,7 +35,7 @@ messaging:
reconnect_wait: 5
buffer_size: 1024
monetix:
mcards:
base_url_env: MONETIX_BASE_URL
project_id_env: MONETIX_PROJECT_ID
secret_key_env: MONETIX_SECRET_KEY
@@ -46,7 +46,7 @@ monetix:
status_processing: "processing"
gateway:
id: "monetix"
id: "mcards"
is_enabled: true
network: "MIR"
currencies: ["RUB"]

View File

@@ -35,7 +35,7 @@ messaging:
reconnect_wait: 5
buffer_size: 1024
monetix:
mcards:
base_url_env: MONETIX_BASE_URL
project_id_env: MONETIX_PROJECT_ID
secret_key_env: MONETIX_SECRET_KEY
@@ -46,7 +46,7 @@ monetix:
status_processing: "processing"
gateway:
id: "monetix"
id: "mcards"
is_enabled: true
network: "MIR"
currencies: ["RUB"]

View File

@@ -32,12 +32,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
@@ -47,9 +47,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -181,8 +181,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -210,8 +210,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -22,6 +22,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
"github.com/tech/sendico/pkg/server/grpcapp"
"go.uber.org/zap"
@@ -41,7 +42,7 @@ type Imp struct {
type config struct {
*grpcapp.Config `yaml:",inline"`
Monetix monetixConfig `yaml:"monetix"`
Monetix monetixConfig `yaml:"mcards"`
Gateway gatewayConfig `yaml:"gateway"`
HTTP httpConfig `yaml:"http"`
}
@@ -216,7 +217,7 @@ func (i *Imp) Start() error {
return gatewaymongo.New(logger, conn)
}
app, err := grpcapp.NewApp(i.logger, "monetix", cfg.Config, i.debug, repoFactory, serviceFactory)
app, err := grpcapp.NewApp(i.logger, paymenttypes.DefaultCardsGatewayID, cfg.Config, i.debug, repoFactory, serviceFactory)
if err != nil {
return err
}
@@ -275,7 +276,7 @@ func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (monetix.Config, error) {
if id, err := strconv.ParseInt(raw, 10, 64); err == nil {
projectID = id
} else {
return monetix.Config{}, merrors.InvalidArgument("invalid project id in env "+cfg.ProjectIDEnv, "monetix.project_id")
return monetix.Config{}, merrors.InvalidArgument("invalid project id in env "+cfg.ProjectIDEnv, "mcards.project_id")
}
}
}
@@ -310,7 +311,7 @@ func (i *Imp) resolveMonetixConfig(cfg monetixConfig) (monetix.Config, error) {
func resolveGatewayDescriptor(cfg gatewayConfig, monetixCfg monetix.Config) *gatewayv1.GatewayInstanceDescriptor {
id := strings.TrimSpace(cfg.ID)
if id == "" {
id = "monetix"
id = paymenttypes.DefaultCardsGatewayID
}
network := strings.ToUpper(strings.TrimSpace(cfg.Network))
@@ -333,7 +334,7 @@ func resolveGatewayDescriptor(cfg gatewayConfig, monetixCfg monetix.Config) *gat
return &gatewayv1.GatewayInstanceDescriptor{
Id: id,
Rail: gatewayv1.Rail_RAIL_CARD_PAYOUT,
Rail: gatewayv1.Rail_RAIL_CARD,
Network: network,
Currencies: currencies,
Capabilities: &gatewayv1.RailCapabilities{
@@ -444,7 +445,7 @@ func (i *Imp) resolveCallbackConfig(cfg callbackConfig) (callbackRuntimeConfig,
}
path := strings.TrimSpace(cfg.Path)
if path == "" {
path = "/monetix/callback"
path = "/" + paymenttypes.DefaultCardsGatewayID + "/callback"
}
maxBody := cfg.MaxBodyBytes
if maxBody <= 0 {

View File

@@ -78,7 +78,7 @@ func (p *cardPayoutProcessor) resolveProjectID(requestProjectID int64, logFieldK
}
if projectID == 0 {
p.logger.Warn("Monetix project_id is not configured", zap.String(logFieldKey, logFieldValue))
return 0, merrors.Internal("monetix project_id is not configured")
return 0, merrors.Internal("mcards project_id is not configured")
}
return projectID, nil
}

View File

@@ -151,9 +151,9 @@ func (s *Service) startDiscoveryAnnouncer() {
return
}
announce := discovery.Announcement{
Service: "CARD_PAYOUT_RAIL_GATEWAY",
Rail: "CARD_PAYOUT",
Operations: []string{"payout.card", "observe.confirm"},
Service: "CARD_RAIL_GATEWAY",
Rail: discovery.RailCardPayout,
Operations: discovery.CardPayoutRailGatewayOperations(),
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
@@ -164,7 +164,7 @@ func (s *Service) startDiscoveryAnnouncer() {
announce.Currencies = currenciesFromDescriptor(s.gatewayDescriptor)
}
if strings.TrimSpace(announce.ID) == "" {
announce.ID = "card_payout_rail_gateway"
announce.ID = discovery.StablePaymentGatewayID(discovery.RailCardPayout)
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.MntxGateway), announce)
s.announcer.Start()

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
@@ -312,10 +313,7 @@ func clearSignature(req any) (func(string), error) {
}
}
func logRequestDeadline(logger *zap.Logger, ctx context.Context, url string) {
if logger == nil {
return
}
func logRequestDeadline(logger mlogger.Logger, ctx context.Context, url string) {
if ctx == nil {
logger.Info("Monetix request context is nil", zap.String("url", url))
return

View File

@@ -36,7 +36,7 @@ messaging:
buffer_size: 1024
gateway:
rail: "provider_settlement"
rail: "SETTLEMENT"
target_chat_id_env: TGSETTLE_GATEWAY_CHAT_ID
timeout_seconds: 345600
accepted_user_ids: []

View File

@@ -36,7 +36,7 @@ messaging:
buffer_size: 1024
gateway:
rail: "provider_settlement"
rail: "SETTLEMENT"
target_chat_id_env: TGSETTLE_GATEWAY_CHAT_ID
timeout_seconds: 345600
accepted_user_ids: []

View File

@@ -30,13 +30,13 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -44,9 +44,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -179,8 +179,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -208,8 +208,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -10,6 +10,7 @@ import (
gatewaymongo "github.com/tech/sendico/gateway/tgsettle/storage/mongo"
"github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/merrors"
msg "github.com/tech/sendico/pkg/messaging"
mb "github.com/tech/sendico/pkg/messaging/broker"
@@ -141,8 +142,12 @@ func (i *Imp) loadConfig() (*config, error) {
if cfg.Metrics == nil {
cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9406"}
}
cfg.Gateway.Rail = discovery.NormalizeRail(cfg.Gateway.Rail)
if cfg.Gateway.Rail == "" {
return nil, merrors.InvalidArgument("gateway rail is required", "gateway.rail")
}
if !discovery.IsKnownRail(cfg.Gateway.Rail) {
return nil, merrors.InvalidArgument("gateway rail must be a known token", "gateway.rail")
}
return cfg, nil
}

View File

@@ -313,7 +313,7 @@ func (s *Service) publishPendingConfirmationResult(pending *storagemodel.Pending
return nil
}
func (s *Service) sendTelegramText(ctx context.Context, request *model.TelegramTextRequest) error {
func (s *Service) sendTelegramText(_ context.Context, request *model.TelegramTextRequest) error {
if request == nil {
return merrors.InvalidArgument("telegram text request is nil", "request")
}

View File

@@ -95,9 +95,12 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
broker: broker,
cfg: cfg,
msgCfg: cfg.MessagingSettings,
rail: strings.TrimSpace(cfg.Rail),
rail: discovery.NormalizeRail(cfg.Rail),
invokeURI: strings.TrimSpace(cfg.InvokeURI),
}
if svc.rail == "" {
svc.rail = strings.TrimSpace(cfg.Rail)
}
svc.chatID = strings.TrimSpace(readEnv(cfg.TargetChatIDEnv))
svc.successReaction = strings.TrimSpace(cfg.SuccessReaction)
if svc.successReaction == "" {
@@ -526,14 +529,12 @@ func (s *Service) startAnnouncer() {
if s == nil || s.producer == nil {
return
}
caps := []string{"telegram_confirmation", "money_persistence", "observe.confirm", "payout.fiat"}
if s.rail != "" {
caps = append(caps, "confirmations."+strings.ToLower(string(mservice.PaymentGateway))+"."+strings.ToLower(s.rail))
}
rail := discovery.RailProviderSettlement
caps := discovery.ProviderSettlementRailGatewayOperations()
announce := discovery.Announcement{
ID: discovery.StablePaymentGatewayID(s.rail),
ID: discovery.StablePaymentGatewayID(rail),
Service: string(mservice.PaymentGateway),
Rail: s.rail,
Rail: rail,
Operations: caps,
InvokeURI: s.invokeURI,
}
@@ -755,27 +756,3 @@ func readEnv(env string) string {
}
var _ grpcapp.Service = (*Service)(nil)
func statusFromConfirmationResult(r *model.ConfirmationResult) storagemodel.PaymentStatus {
if r == nil {
return storagemodel.PaymentStatusWaiting
}
switch r.Status {
case model.ConfirmationStatusConfirmed:
return storagemodel.PaymentStatusProcessing
case model.ConfirmationStatusClarified:
return storagemodel.PaymentStatusWaiting
case model.ConfirmationStatusRejected:
return storagemodel.PaymentStatusFailed
case model.ConfirmationStatusTimeout:
return storagemodel.PaymentStatusFailed
default:
return storagemodel.PaymentStatusFailed
}
}

View File

@@ -7,7 +7,7 @@ replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/gateway/common => ../common
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1
github.com/ethereum/go-ethereum v1.17.0
github.com/fbsobreira/gotron-sdk v0.24.1
github.com/hashicorp/vault/api v1.22.0
@@ -27,7 +27,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260218040609-6f1c0c95351b // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
@@ -42,7 +42,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -65,7 +65,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pborman/uuid v1.2.1 // indirect
@@ -73,7 +73,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/rjeczalik/notify v0.9.3 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
@@ -94,11 +94,11 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260218040609-6f1c0c95351b h1:RVnS+OZmBJbbNeqejAksq3Mxc73y0IEzyTUHPPWZuj8=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260218040609-6f1c0c95351b/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc h1:1stW1OipdBj8Me+nj26SzT8yil7OYve0r3cWobzk1JQ=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -64,8 +64,8 @@ github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+Zlfu
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
@@ -76,8 +76,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes=
@@ -211,8 +211,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -245,8 +245,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -339,8 +339,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -374,10 +374,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -229,8 +229,8 @@ func (s *Service) startDiscoveryAnnouncers() {
announce := discovery.Announcement{
ID: discovery.StableCryptoRailGatewayID(network.Name.String()),
Service: "CRYPTO_RAIL_GATEWAY",
Rail: "CRYPTO",
Operations: []string{"balance.read", "payin.crypto", "payout.crypto", "fee.send", "observe.confirm"},
Rail: discovery.RailCrypto,
Operations: discovery.CryptoRailGatewayOperations(),
Currencies: currencies,
InvokeURI: s.invokeURI,
Version: version,

View File

@@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/ledgerconv"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model/account_role"
@@ -23,7 +24,36 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
const ledgerConnectorID = "ledger"
const (
ledgerConnectorID = "ledger"
ledgerRailName = "LEDGER"
opParamOperation = "operation"
opParamToMoney = "to_money"
opParamAmount = "amount"
opParamCurrency = "currency"
opParamOrganizationRef = "organization_ref"
opParamAccountType = "account_type"
opParamStatus = "status"
opParamAllowNegative = "allow_negative"
opParamRole = "role"
opParamDescription = "description"
opParamMetadata = "metadata"
opParamCharges = "charges"
opParamEventTime = "event_time"
opParamContraLedgerAccountRef = "contra_ledger_account_ref"
opParamLedgerAccountRef = "ledger_account_ref"
opParamLineType = "line_type"
opParamAccountCode = "account_code"
opParamIsSettlement = "is_settlement"
txMetaPaymentPlanID = "payment_plan_id"
txMetaFromRail = "from_rail"
txMetaToRail = "to_rail"
txMetaExternalReference = "external_reference_id"
txMetaFXRateUsed = "fx_rate_used"
txMetaFeeAmount = "fee_amount"
)
// Client exposes typed helpers around the ledger gRPC API.
type Client interface {
@@ -36,6 +66,8 @@ type Client interface {
ListConnectorAccounts(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error)
PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error)
@@ -148,7 +180,7 @@ func (c *ledgerClient) CreateTransaction(ctx context.Context, tx rail.LedgerTx)
metadata := ledgerTxMetadata(tx.Metadata, tx)
extraParams := map[string]interface{}{}
if op := strings.TrimSpace(tx.Operation); op != "" {
extraParams["operation"] = op
extraParams[opParamOperation] = op
}
if len(extraParams) == 0 {
extraParams = nil
@@ -204,13 +236,13 @@ func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAc
return nil, merrors.InvalidArgument("ledger: currency is required")
}
params := map[string]interface{}{
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
"account_type": req.GetAccountType().String(),
"status": req.GetStatus().String(),
"allow_negative": req.GetAllowNegative(),
opParamOrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
opParamAccountType: req.GetAccountType().String(),
opParamStatus: req.GetStatus().String(),
opParamAllowNegative: req.GetAllowNegative(),
}
if role := req.GetRole(); role != ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
params["role"] = role.String()
params[opParamRole] = role.String()
}
label := ""
if desc := req.GetDescribable(); desc != nil {
@@ -218,12 +250,12 @@ func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAc
if desc.Description != nil {
trimmed := strings.TrimSpace(desc.GetDescription())
if trimmed != "" {
params["description"] = trimmed
params[opParamDescription] = trimmed
}
}
}
if len(req.GetMetadata()) > 0 {
params["metadata"] = mapStringToInterface(req.GetMetadata())
params[opParamMetadata] = mapStringToInterface(req.GetMetadata())
}
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
@@ -277,6 +309,30 @@ func (c *ledgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.P
return c.submitLedgerOperation(ctx, connectorv1.OperationType_DEBIT, req.GetLedgerAccountRef(), "", req.GetMoney(), req)
}
func (c *ledgerClient) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
return c.submitLedgerOperationWithExtras(
ctx,
connectorv1.OperationType_CREDIT,
"",
req.GetLedgerAccountRef(),
req.GetMoney(),
req,
map[string]interface{}{opParamOperation: discovery.OperationExternalCredit},
)
}
func (c *ledgerClient) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
return c.submitLedgerOperationWithExtras(
ctx,
connectorv1.OperationType_DEBIT,
req.GetLedgerAccountRef(),
"",
req.GetMoney(),
req,
map[string]interface{}{opParamOperation: discovery.OperationExternalDebit},
)
}
func (c *ledgerClient) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
return c.submitLedgerOperation(ctx, connectorv1.OperationType_TRANSFER, req.GetFromLedgerAccountRef(), req.GetToLedgerAccountRef(), req.GetMoney(), req)
}
@@ -292,7 +348,7 @@ func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXR
}
params := ledgerOperationParams(req.GetOrganizationRef(), req.GetDescription(), req.GetMetadata(), req.GetCharges(), req.GetEventTime())
params["rate"] = strings.TrimSpace(req.GetRate())
params["to_money"] = map[string]interface{}{"amount": req.GetToMoney().GetAmount(), "currency": req.GetToMoney().GetCurrency()}
params[opParamToMoney] = map[string]interface{}{opParamAmount: req.GetToMoney().GetAmount(), opParamCurrency: req.GetToMoney().GetCurrency()}
operation := &connectorv1.Operation{
Type: connectorv1.OperationType_FX,
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
@@ -466,7 +522,7 @@ func (c *ledgerClient) submitLedgerOperationWithExtras(ctx context.Context, opTy
params := ledgerOperationParams(orgRef, description, metadata, charges, eventTime)
if contraRef != "" {
params["contra_ledger_account_ref"] = strings.TrimSpace(contraRef)
params[opParamContraLedgerAccountRef] = strings.TrimSpace(contraRef)
}
if len(extraParams) > 0 {
for key, value := range extraParams {
@@ -534,17 +590,17 @@ func accountRoleFromLedgerProto(role ledgerv1.AccountRole) account_role.AccountR
func ledgerOperationParams(orgRef, description string, metadata map[string]string, charges []*ledgerv1.PostingLine, eventTime *timestamppb.Timestamp) map[string]interface{} {
params := map[string]interface{}{
"organization_ref": strings.TrimSpace(orgRef),
"description": strings.TrimSpace(description),
opParamOrganizationRef: strings.TrimSpace(orgRef),
opParamDescription: strings.TrimSpace(description),
}
if len(metadata) > 0 {
params["metadata"] = mapStringToInterface(metadata)
params[opParamMetadata] = mapStringToInterface(metadata)
}
if len(charges) > 0 {
params["charges"] = chargesToInterface(charges)
params[opParamCharges] = chargesToInterface(charges)
}
if eventTime != nil {
params["event_time"] = eventTime.AsTime().UTC().Format(time.RFC3339Nano)
params[opParamEventTime] = eventTime.AsTime().UTC().Format(time.RFC3339Nano)
}
return params
}
@@ -580,25 +636,25 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
details = account.GetProviderDetails().AsMap()
}
accountType := ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
if v := strings.TrimSpace(fmt.Sprint(details["account_type"])); v != "" {
if v := strings.TrimSpace(fmt.Sprint(details[opParamAccountType])); v != "" {
accountType = parseAccountType(v)
}
status := ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
if v := strings.TrimSpace(fmt.Sprint(details["status"])); v != "" {
if v := strings.TrimSpace(fmt.Sprint(details[opParamStatus])); v != "" {
status = parseAccountStatus(v)
}
allowNegative := false
if v, ok := details["allow_negative"].(bool); ok {
if v, ok := details[opParamAllowNegative].(bool); ok {
allowNegative = v
}
role := ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
if v := strings.TrimSpace(fmt.Sprint(details["role"])); v != "" {
if v := strings.TrimSpace(fmt.Sprint(details[opParamRole])); v != "" {
if parsed, ok := ledgerconv.ParseAccountRole(v); ok {
role = parsed
}
}
if role == ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
switch v := details["is_settlement"].(type) {
switch v := details[opParamIsSettlement].(type) {
case bool:
if v {
role = ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
@@ -609,13 +665,13 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
}
}
}
accountCode := strings.TrimSpace(fmt.Sprint(details["account_code"]))
accountCode := strings.TrimSpace(fmt.Sprint(details[opParamAccountCode]))
accountID := ""
if ref := account.GetRef(); ref != nil {
accountID = strings.TrimSpace(ref.GetAccountId())
}
organizationRef := strings.TrimSpace(account.GetOwnerRef())
if v := strings.TrimSpace(fmt.Sprint(details["organization_ref"])); v != "" {
if v := strings.TrimSpace(fmt.Sprint(details[opParamOrganizationRef])); v != "" {
organizationRef = v
}
describable := account.GetDescribable()
@@ -674,7 +730,7 @@ func operationDescription(op *connectorv1.Operation) string {
if op == nil || op.GetParams() == nil {
return ""
}
if value, ok := op.GetParams().AsMap()["description"]; ok {
if value, ok := op.GetParams().AsMap()[opParamDescription]; ok {
return strings.TrimSpace(fmt.Sprint(value))
}
return ""
@@ -731,10 +787,10 @@ func chargesToInterface(charges []*ledgerv1.PostingLine) []interface{} {
continue
}
result = append(result, map[string]interface{}{
"ledger_account_ref": strings.TrimSpace(line.GetLedgerAccountRef()),
"amount": strings.TrimSpace(line.GetMoney().GetAmount()),
"currency": strings.TrimSpace(line.GetMoney().GetCurrency()),
"line_type": line.GetLineType().String(),
opParamLedgerAccountRef: strings.TrimSpace(line.GetLedgerAccountRef()),
opParamAmount: strings.TrimSpace(line.GetMoney().GetAmount()),
opParamCurrency: strings.TrimSpace(line.GetMoney().GetCurrency()),
opParamLineType: line.GetLineType().String(),
})
}
if len(result) == 0 {
@@ -793,7 +849,7 @@ func (c *ledgerClient) callContext(ctx context.Context) (context.Context, contex
}
func isLedgerRail(value string) bool {
return strings.EqualFold(strings.TrimSpace(value), "LEDGER")
return strings.EqualFold(strings.TrimSpace(value), ledgerRailName)
}
func cloneMoney(input *moneyv1.Money) *moneyv1.Money {
@@ -823,22 +879,22 @@ func ledgerTxMetadata(base map[string]string, tx rail.LedgerTx) map[string]strin
meta = map[string]string{}
}
if val := strings.TrimSpace(tx.PaymentPlanID); val != "" {
meta["payment_plan_id"] = val
meta[txMetaPaymentPlanID] = val
}
if val := strings.TrimSpace(tx.FromRail); val != "" {
meta["from_rail"] = val
meta[txMetaFromRail] = val
}
if val := strings.TrimSpace(tx.ToRail); val != "" {
meta["to_rail"] = val
meta[txMetaToRail] = val
}
if val := strings.TrimSpace(tx.ExternalReferenceID); val != "" {
meta["external_reference_id"] = val
meta[txMetaExternalReference] = val
}
if val := strings.TrimSpace(tx.FXRateUsed); val != "" {
meta["fx_rate_used"] = val
meta[txMetaFXRateUsed] = val
}
if val := strings.TrimSpace(tx.FeeAmount); val != "" {
meta["fee_amount"] = val
meta[txMetaFeeAmount] = val
}
if len(meta) == 0 {
return nil

View File

@@ -6,6 +6,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tech/sendico/pkg/discovery"
accountrolev1 "github.com/tech/sendico/pkg/proto/common/account_role/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
@@ -92,3 +94,65 @@ func TestTransferInternal_SubmitsTransferOperation(t *testing.T) {
assert.Equal(t, "op-1", resp.GetJournalEntryRef())
assert.Equal(t, ledgerv1.EntryType_ENTRY_TRANSFER, resp.GetEntryType())
}
func TestPostExternalCreditWithCharges_SubmitsExternalOperation(t *testing.T) {
ctx := context.Background()
var captured *connectorv1.Operation
stub := &stubConnector{
submitFn: func(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
captured = req.GetOperation()
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{OperationId: "op-ext-credit"}}, nil
},
}
client := NewWithClient(Config{}, stub)
resp, err := client.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
IdempotencyKey: "id-ext-credit",
OrganizationRef: "org-1",
Money: &moneyv1.Money{Currency: "USDT", Amount: "1.0"},
Role: ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, captured)
assert.Equal(t, connectorv1.OperationType_CREDIT, captured.GetType())
assert.Equal(t, "", captured.GetTo().GetAccount().GetAccountId())
assert.Equal(t, accountrolev1.AccountRole_OPERATING, captured.GetToRole())
assert.Equal(t, discovery.OperationExternalCredit, captured.GetParams().AsMap()["operation"])
assert.Equal(t, "op-ext-credit", resp.GetJournalEntryRef())
assert.Equal(t, ledgerv1.EntryType_ENTRY_CREDIT, resp.GetEntryType())
}
func TestPostExternalDebitWithCharges_SubmitsExternalOperation(t *testing.T) {
ctx := context.Background()
var captured *connectorv1.Operation
stub := &stubConnector{
submitFn: func(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
captured = req.GetOperation()
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{OperationId: "op-ext-debit"}}, nil
},
}
client := NewWithClient(Config{}, stub)
resp, err := client.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
IdempotencyKey: "id-ext-debit",
OrganizationRef: "org-1",
Money: &moneyv1.Money{Currency: "RUB", Amount: "77.14"},
Role: ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, captured)
assert.Equal(t, connectorv1.OperationType_DEBIT, captured.GetType())
assert.Equal(t, "", captured.GetFrom().GetAccount().GetAccountId())
assert.Equal(t, accountrolev1.AccountRole_HOLD, captured.GetFromRole())
assert.Equal(t, discovery.OperationExternalDebit, captured.GetParams().AsMap()["operation"])
assert.Equal(t, "op-ext-debit", resp.GetJournalEntryRef())
assert.Equal(t, ledgerv1.EntryType_ENTRY_DEBIT, resp.GetEntryType())
}

View File

@@ -4,8 +4,8 @@ import (
"context"
"github.com/tech/sendico/pkg/payments/rail"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
)
@@ -19,6 +19,8 @@ type Fake struct {
ListConnectorAccountsFn func(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error)
PostCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
PostDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
PostExternalCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
PostExternalDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
TransferInternalFn func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
ApplyFXWithChargesFn func(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error)
BlockAccountFn func(ctx context.Context, req *ledgerv1.BlockAccountRequest) (*ledgerv1.BlockAccountResponse, error)
@@ -85,6 +87,20 @@ func (f *Fake) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebit
return &ledgerv1.PostResponse{}, nil
}
func (f *Fake) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
if f.PostExternalCreditWithChargesFn != nil {
return f.PostExternalCreditWithChargesFn(ctx, req)
}
return &ledgerv1.PostResponse{}, nil
}
func (f *Fake) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
if f.PostExternalDebitWithChargesFn != nil {
return f.PostExternalDebitWithChargesFn(ctx, req)
}
return &ledgerv1.PostResponse{}, nil
}
func (f *Fake) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
if f.TransferInternalFn != nil {
return f.TransferInternalFn(ctx, req)

View File

@@ -31,13 +31,13 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -45,9 +45,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -181,8 +181,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -210,8 +210,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/ledger/internal/appversion"
"github.com/tech/sendico/pkg/connector/params"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/ledgerconv"
"github.com/tech/sendico/pkg/merrors"
accountrolev1 "github.com/tech/sendico/pkg/proto/common/account_role/v1"
@@ -16,6 +17,7 @@ import (
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -222,7 +224,7 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
if err != nil {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
}
operation := strings.ToLower(strings.TrimSpace(reader.String("operation")))
operation := discovery.NormalizeOperation(reader.String("operation"))
switch op.GetType() {
case connectorv1.OperationType_CREDIT:
@@ -230,11 +232,11 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
if accountID == "" && op.GetToRole() == accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "credit: to.account or to_role is required", op, "")}}, nil
}
if operation != "" && operation != "external.credit" {
if operation != "" && operation != discovery.OperationExternalCredit {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "credit: unsupported operation override", op, "")}}, nil
}
creditFn := c.svc.PostCreditWithCharges
if operation == "external.credit" {
if operation == discovery.OperationExternalCredit {
creditFn = c.svc.PostExternalCreditWithCharges
}
resp, err := creditFn(ctx, &ledgerv1.PostCreditRequest{
@@ -250,6 +252,10 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
Role: accountRoleFromConnectorRole(op.GetToRole()),
})
if err != nil {
c.svc.logger.Warn("Operation failed", zap.Error(err), zap.String("operation", operation),
zap.String("idempotency_key", op.IdempotencyKey), zap.String("description", description),
zap.String("organization_ref", orgRef), zap.String("ledger_account_ref", accountID),
)
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, accountID)}}, nil
}
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_OPERATION_SUCCESS)}, nil
@@ -258,11 +264,11 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
if accountID == "" && op.GetFromRole() == accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "debit: from.account or from_role is required", op, "")}}, nil
}
if operation != "" && operation != "external.debit" {
if operation != "" && operation != discovery.OperationExternalDebit {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "debit: unsupported operation override", op, "")}}, nil
}
debitFn := c.svc.PostDebitWithCharges
if operation == "external.debit" {
if operation == discovery.OperationExternalDebit {
debitFn = c.svc.PostExternalDebitWithCharges
}
resp, err := debitFn(ctx, &ledgerv1.PostDebitRequest{
@@ -393,14 +399,14 @@ func ledgerOperationParams() []*connectorv1.OperationParamSpec {
Type: connectorv1.ParamType_STRING,
Required: false,
Description: "Optional ledger operation override (external.credit).",
AllowedValues: []string{"external.credit"},
AllowedValues: []string{discovery.OperationExternalCredit},
}
externalDebit := &connectorv1.ParamSpec{
Key: "operation",
Type: connectorv1.ParamType_STRING,
Required: false,
Description: "Optional ledger operation override (external.debit).",
AllowedValues: []string{"external.debit"},
AllowedValues: []string{discovery.OperationExternalDebit},
}
return []*connectorv1.OperationParamSpec{
{OperationType: connectorv1.OperationType_CREDIT, Params: append(common, externalCredit, &connectorv1.ParamSpec{Key: "contra_ledger_account_ref", Type: connectorv1.ParamType_STRING, Required: false})},

View File

@@ -7,6 +7,40 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
)
type journalEntryType string
const (
journalEntryTypeCredit journalEntryType = "credit"
journalEntryTypeDebit journalEntryType = "debit"
journalEntryTypeTransfer journalEntryType = "transfer"
journalEntryTypeFX journalEntryType = "fx"
)
type journalEntryStatus string
const (
journalEntryStatusAttempted journalEntryStatus = "attempted"
journalEntryStatusSuccess journalEntryStatus = "success"
journalEntryStatusError journalEntryStatus = "error"
)
type journalEntryErrorType string
const (
journalEntryErrorNotImplemented journalEntryErrorType = "not_implemented"
journalEntryErrorFailed journalEntryErrorType = "failed"
journalEntryErrorIdempotencyCheck journalEntryErrorType = "idempotency_check_failed"
journalEntryErrorAccountResolve journalEntryErrorType = "account_resolve_failed"
journalEntryErrorAccountInvalid journalEntryErrorType = "account_invalid"
journalEntryErrorContraResolve journalEntryErrorType = "contra_resolve_failed"
journalEntryErrorContraMissingID journalEntryErrorType = "contra_missing_id"
journalEntryErrorSystemAccountResolve journalEntryErrorType = "system_account_resolve_failed"
journalEntryErrorSystemAccountInvalid journalEntryErrorType = "system_account_invalid"
journalEntryErrorSystemAccountMissing journalEntryErrorType = "system_account_missing_id"
journalEntryErrorUnbalancedAfterContra journalEntryErrorType = "unbalanced_after_contra"
journalEntryErrorTransactionFailed journalEntryErrorType = "transaction_failed"
)
var (
metricsOnce sync.Once
@@ -110,16 +144,16 @@ func initMetrics() {
// Metric recording helpers
func recordJournalEntry(entryType, status string, durationSeconds float64) {
func recordJournalEntry(entryType journalEntryType, status journalEntryStatus, durationSeconds float64) {
initMetrics()
journalEntriesTotal.WithLabelValues(entryType, status).Inc()
journalEntryLatency.WithLabelValues(entryType).Observe(durationSeconds)
journalEntriesTotal.WithLabelValues(string(entryType), string(status)).Inc()
journalEntryLatency.WithLabelValues(string(entryType)).Observe(durationSeconds)
}
func recordJournalEntryError(entryType, errorType string) {
func recordJournalEntryError(entryType journalEntryType, errorType journalEntryErrorType) {
initMetrics()
journalEntryErrors.WithLabelValues(entryType, errorType).Inc()
journalEntriesTotal.WithLabelValues(entryType, "error").Inc()
journalEntryErrors.WithLabelValues(string(entryType), string(errorType)).Inc()
journalEntriesTotal.WithLabelValues(string(entryType), string(journalEntryStatusError)).Inc()
}
func recordBalanceQuery(status string, durationSeconds float64) {
@@ -128,9 +162,9 @@ func recordBalanceQuery(status string, durationSeconds float64) {
balanceQueryLatency.WithLabelValues(status).Observe(durationSeconds)
}
func recordTransactionAmount(currency, entryType string, amount float64) {
func recordTransactionAmount(currency string, entryType journalEntryType, amount float64) {
initMetrics()
transactionAmounts.WithLabelValues(currency, entryType).Observe(amount)
transactionAmounts.WithLabelValues(currency, string(entryType)).Observe(amount)
}
func recordAccountOperation(operation, status string) {
@@ -138,7 +172,7 @@ func recordAccountOperation(operation, status string) {
accountOperationsTotal.WithLabelValues(operation, status).Inc()
}
func recordDuplicateRequest(entryType string) {
func recordDuplicateRequest(entryType journalEntryType) {
initMetrics()
duplicateRequestsTotal.WithLabelValues(entryType).Inc()
duplicateRequestsTotal.WithLabelValues(string(entryType)).Inc()
}

View File

@@ -65,7 +65,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil {
recordDuplicateRequest("credit")
recordDuplicateRequest(journalEntryTypeCredit)
logger.Info("Duplicate credit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{
@@ -75,18 +75,18 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
recordJournalEntryError("credit", "idempotency_check_failed")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorIdempotencyCheck)
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
}
account, accountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.LedgerAccountRef), roleModel, orgRef, req.Money.Currency, "account")
if err != nil {
recordJournalEntryError("credit", "account_resolve_failed")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorAccountResolve)
return nil, err
}
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
recordJournalEntryError("credit", "account_invalid")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorAccountInvalid)
return nil, err
}
@@ -159,12 +159,12 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
contraAccount, err := s.resolveSettlementAccount(ctx, orgRef, req.Money.Currency, req.ContraLedgerAccountRef, accountsByRef)
if err != nil {
recordJournalEntryError("credit", "contra_resolve_failed")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorContraResolve)
return nil, err
}
contraAccountID := contraAccount.GetID()
if contraAccountID == nil {
recordJournalEntryError("credit", "contra_missing_id")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorContraMissingID)
return nil, merrors.Internal("contra account missing identifier")
}
@@ -183,7 +183,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
}
if !entryTotal.IsZero() {
recordJournalEntryError("credit", "unbalanced_after_contra")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorUnbalancedAfterContra)
return nil, merrors.Internal("failed to balance journal entry")
}
@@ -237,13 +237,13 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
})
if err != nil {
recordJournalEntryError("credit", "transaction_failed")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorTransactionFailed)
return nil, err
}
amountFloat, _ := creditAmount.Float64()
recordTransactionAmount(req.Money.Currency, "credit", amountFloat)
recordJournalEntry("credit", "success", 0)
recordTransactionAmount(req.Money.Currency, journalEntryTypeCredit, amountFloat)
recordJournalEntry(journalEntryTypeCredit, journalEntryStatusSuccess, 0)
return result.(*ledgerv1.PostResponse), nil
}
}

View File

@@ -63,7 +63,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil {
recordDuplicateRequest("debit")
recordDuplicateRequest(journalEntryTypeDebit)
logger.Info("Duplicate debit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{
@@ -79,11 +79,11 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
account, accountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.LedgerAccountRef), roleModel, orgRef, req.Money.Currency, "account")
if err != nil {
recordJournalEntryError("debit", "account_resolve_failed")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorAccountResolve)
return nil, err
}
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
recordJournalEntryError("debit", "account_invalid")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorAccountInvalid)
return nil, err
}
@@ -156,12 +156,12 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
contraAccount, err := s.resolveSettlementAccount(ctx, orgRef, req.Money.Currency, req.ContraLedgerAccountRef, accountsByRef)
if err != nil {
recordJournalEntryError("debit", "contra_resolve_failed")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorContraResolve)
return nil, err
}
contraAccountID := contraAccount.GetID()
if contraAccountID == nil {
recordJournalEntryError("debit", "contra_missing_id")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorContraMissingID)
return nil, merrors.Internal("contra account missing identifier")
}
@@ -180,7 +180,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
}
if !entryTotal.IsZero() {
recordJournalEntryError("debit", "unbalanced_after_contra")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorUnbalancedAfterContra)
return nil, merrors.Internal("failed to balance journal entry")
}
@@ -234,13 +234,13 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
})
if err != nil {
recordJournalEntryError("debit", "transaction_failed")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorTransactionFailed)
return nil, err
}
amountFloat, _ := debitAmount.Float64()
recordTransactionAmount(req.Money.Currency, "debit", amountFloat)
recordJournalEntry("debit", "success", 0)
recordTransactionAmount(req.Money.Currency, journalEntryTypeDebit, amountFloat)
recordJournalEntry(journalEntryTypeDebit, journalEntryStatusSuccess, 0)
return result.(*ledgerv1.PostResponse), nil
}
}

View File

@@ -60,7 +60,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil {
recordDuplicateRequest("credit")
recordDuplicateRequest(journalEntryTypeCredit)
logger.Info("Duplicate external credit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{
@@ -70,34 +70,34 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
recordJournalEntryError("credit", "idempotency_check_failed")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorIdempotencyCheck)
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
}
account, accountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.LedgerAccountRef), roleModel, orgRef, req.Money.Currency, "account")
if err != nil {
recordJournalEntryError("credit", "account_resolve_failed")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorAccountResolve)
return nil, err
}
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
recordJournalEntryError("credit", "account_invalid")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorAccountInvalid)
return nil, err
}
systemAccount, err := s.systemAccount(ctx, pmodel.SystemAccountPurposeExternalSource, req.Money.Currency)
if err != nil {
recordJournalEntryError("credit", "system_account_resolve_failed")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorSystemAccountResolve)
return nil, err
}
if err := validateSystemAccount(systemAccount, pmodel.SystemAccountPurposeExternalSource, req.Money.Currency); err != nil {
recordJournalEntryError("credit", "system_account_invalid")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorSystemAccountInvalid)
return nil, err
}
systemAccountID := systemAccount.GetID()
if systemAccountID == nil {
recordJournalEntryError("credit", "system_account_missing_id")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorSystemAccountMissing)
return nil, merrors.Internal("system account missing identifier")
}
@@ -186,7 +186,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
}
if !entryTotal.IsZero() {
recordJournalEntryError("credit", "unbalanced_after_contra")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorUnbalancedAfterContra)
return nil, merrors.Internal("failed to balance journal entry")
}
@@ -240,13 +240,13 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
})
if err != nil {
recordJournalEntryError("credit", "transaction_failed")
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorTransactionFailed)
return nil, err
}
amountFloat, _ := creditAmount.Float64()
recordTransactionAmount(req.Money.Currency, "credit", amountFloat)
recordJournalEntry("credit", "success", 0)
recordTransactionAmount(req.Money.Currency, journalEntryTypeCredit, amountFloat)
recordJournalEntry(journalEntryTypeCredit, journalEntryStatusSuccess, 0)
return result.(*ledgerv1.PostResponse), nil
}
}
@@ -293,7 +293,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil {
recordDuplicateRequest("debit")
recordDuplicateRequest(journalEntryTypeDebit)
logger.Info("Duplicate external debit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{
@@ -303,34 +303,34 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
recordJournalEntryError("debit", "idempotency_check_failed")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorIdempotencyCheck)
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
}
account, accountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.LedgerAccountRef), roleModel, orgRef, req.Money.Currency, "account")
if err != nil {
recordJournalEntryError("debit", "account_resolve_failed")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorAccountResolve)
return nil, err
}
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
recordJournalEntryError("debit", "account_invalid")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorAccountInvalid)
return nil, err
}
systemAccount, err := s.systemAccount(ctx, pmodel.SystemAccountPurposeExternalSink, req.Money.Currency)
if err != nil {
recordJournalEntryError("debit", "system_account_resolve_failed")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorSystemAccountResolve)
return nil, err
}
if err := validateSystemAccount(systemAccount, pmodel.SystemAccountPurposeExternalSink, req.Money.Currency); err != nil {
recordJournalEntryError("debit", "system_account_invalid")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorSystemAccountInvalid)
return nil, err
}
systemAccountID := systemAccount.GetID()
if systemAccountID == nil {
recordJournalEntryError("debit", "system_account_missing_id")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorSystemAccountMissing)
return nil, merrors.Internal("system account missing identifier")
}
@@ -419,7 +419,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
}
if !entryTotal.IsZero() {
recordJournalEntryError("debit", "unbalanced_after_contra")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorUnbalancedAfterContra)
return nil, merrors.Internal("failed to balance journal entry")
}
@@ -473,13 +473,13 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
})
if err != nil {
recordJournalEntryError("debit", "transaction_failed")
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorTransactionFailed)
return nil, err
}
amountFloat, _ := debitAmount.Float64()
recordTransactionAmount(req.Money.Currency, "debit", amountFloat)
recordJournalEntry("debit", "success", 0)
recordTransactionAmount(req.Money.Currency, journalEntryTypeDebit, amountFloat)
recordJournalEntry(journalEntryTypeDebit, journalEntryStatusSuccess, 0)
return result.(*ledgerv1.PostResponse), nil
}
}

View File

@@ -76,7 +76,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
// Check for duplicate idempotency key
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil {
recordDuplicateRequest("fx")
recordDuplicateRequest(journalEntryTypeFX)
logger.Info("Duplicate FX request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{
@@ -244,15 +244,15 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
})
if err != nil {
recordJournalEntryError("fx", "transaction_failed")
recordJournalEntryError(journalEntryTypeFX, journalEntryErrorTransactionFailed)
return nil, err
}
fromAmountFloat, _ := fromAmount.Float64()
toAmountFloat, _ := toAmount.Float64()
recordTransactionAmount(req.FromMoney.Currency, "fx", fromAmountFloat)
recordTransactionAmount(req.ToMoney.Currency, "fx", toAmountFloat)
recordJournalEntry("fx", "success", 0)
recordTransactionAmount(req.FromMoney.Currency, journalEntryTypeFX, fromAmountFloat)
recordTransactionAmount(req.ToMoney.Currency, journalEntryTypeFX, toAmountFloat)
recordJournalEntry(journalEntryTypeFX, journalEntryStatusSuccess, 0)
return result.(*ledgerv1.PostResponse), nil
}
}

View File

@@ -86,7 +86,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
// Check for duplicate idempotency key
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil {
recordDuplicateRequest("transfer")
recordDuplicateRequest(journalEntryTypeTransfer)
logger.Info("Duplicate transfer request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{
@@ -246,13 +246,13 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
})
if err != nil {
recordJournalEntryError("transfer", "failed")
recordJournalEntryError(journalEntryTypeTransfer, journalEntryErrorFailed)
return nil, err
}
amountFloat, _ := transferAmount.Float64()
recordTransactionAmount(req.Money.Currency, "transfer", amountFloat)
recordJournalEntry("transfer", "success", 0)
recordTransactionAmount(req.Money.Currency, journalEntryTypeTransfer, amountFloat)
recordJournalEntry(journalEntryTypeTransfer, journalEntryStatusSuccess, 0)
return result.(*ledgerv1.PostResponse), nil
}
}

View File

@@ -77,7 +77,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.
initMetrics()
service := &Service{
logger: logger.Named("ledger"),
logger: logger.Named("service"),
storage: repo,
producer: prod,
msgCfg: msgCfg,
@@ -117,17 +117,10 @@ func (s *Service) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccount
func (s *Service) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
start := time.Now()
defer func() {
recordJournalEntry("credit", "attempted", time.Since(start).Seconds())
recordJournalEntry(journalEntryTypeCredit, journalEntryStatusAttempted, time.Since(start).Seconds())
}()
responder := s.postCreditResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError("credit", "not_implemented")
}
logger := s.logger.With(zap.String("operation", "credit"))
logger := s.logger.With(zap.String("operation", discovery.OperationLedgerCredit))
if req != nil {
logger = logger.With(
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
@@ -147,7 +140,16 @@ func (s *Service) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostC
logger = logger.With(zap.String("contra_ledger_account_ref", contra))
}
}
s.logLedgerOperation("credit", logger, resp, err)
s.logLedgerOperationStart(discovery.OperationLedgerCredit, logger)
responder := s.postCreditResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorNotImplemented)
}
s.logLedgerOperation(discovery.OperationLedgerCredit, logger, resp, err, time.Since(start))
return resp, err
}
@@ -156,17 +158,10 @@ func (s *Service) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostC
func (s *Service) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
start := time.Now()
defer func() {
recordJournalEntry("credit", "attempted", time.Since(start).Seconds())
recordJournalEntry(journalEntryTypeCredit, journalEntryStatusAttempted, time.Since(start).Seconds())
}()
responder := s.postExternalCreditResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError("credit", "failed")
}
logger := s.logger.With(zap.String("operation", "external_credit"))
logger := s.logger.With(zap.String("operation", discovery.OperationExternalCredit))
if req != nil {
logger = logger.With(
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
@@ -183,7 +178,16 @@ func (s *Service) PostExternalCreditWithCharges(ctx context.Context, req *ledger
logger = logger.With(zap.String("role", role.String()))
}
}
s.logLedgerOperation("external_credit", logger, resp, err)
s.logLedgerOperationStart(discovery.OperationExternalCredit, logger)
responder := s.postExternalCreditResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorFailed)
}
s.logLedgerOperation(discovery.OperationExternalCredit, logger, resp, err, time.Since(start))
return resp, err
}
@@ -192,17 +196,10 @@ func (s *Service) PostExternalCreditWithCharges(ctx context.Context, req *ledger
func (s *Service) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
start := time.Now()
defer func() {
recordJournalEntry("debit", "attempted", time.Since(start).Seconds())
recordJournalEntry(journalEntryTypeDebit, journalEntryStatusAttempted, time.Since(start).Seconds())
}()
responder := s.postDebitResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError("debit", "failed")
}
logger := s.logger.With(zap.String("operation", "debit"))
logger := s.logger.With(zap.String("operation", discovery.OperationLedgerDebit))
if req != nil {
logger = logger.With(
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
@@ -222,7 +219,16 @@ func (s *Service) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDe
logger = logger.With(zap.String("contra_ledger_account_ref", contra))
}
}
s.logLedgerOperation("debit", logger, resp, err)
s.logLedgerOperationStart(discovery.OperationLedgerDebit, logger)
responder := s.postDebitResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorFailed)
}
s.logLedgerOperation(discovery.OperationLedgerDebit, logger, resp, err, time.Since(start))
return resp, err
}
@@ -231,17 +237,10 @@ func (s *Service) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDe
func (s *Service) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
start := time.Now()
defer func() {
recordJournalEntry("debit", "attempted", time.Since(start).Seconds())
recordJournalEntry(journalEntryTypeDebit, journalEntryStatusAttempted, time.Since(start).Seconds())
}()
responder := s.postExternalDebitResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError("debit", "failed")
}
logger := s.logger.With(zap.String("operation", "external_debit"))
logger := s.logger.With(zap.String("operation", discovery.OperationExternalDebit))
if req != nil {
logger = logger.With(
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
@@ -258,7 +257,16 @@ func (s *Service) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv
logger = logger.With(zap.String("role", role.String()))
}
}
s.logLedgerOperation("external_debit", logger, resp, err)
s.logLedgerOperationStart(discovery.OperationExternalDebit, logger)
responder := s.postExternalDebitResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorFailed)
}
s.logLedgerOperation(discovery.OperationExternalDebit, logger, resp, err, time.Since(start))
return resp, err
}
@@ -267,17 +275,10 @@ func (s *Service) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv
func (s *Service) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
start := time.Now()
defer func() {
recordJournalEntry("transfer", "attempted", time.Since(start).Seconds())
recordJournalEntry(journalEntryTypeTransfer, journalEntryStatusAttempted, time.Since(start).Seconds())
}()
responder := s.transferResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError("transfer", "failed")
}
logger := s.logger.With(zap.String("operation", "transfer"))
logger := s.logger.With(zap.String("operation", discovery.OperationLedgerTransfer))
if req != nil {
logger = logger.With(
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
@@ -298,7 +299,16 @@ func (s *Service) TransferInternal(ctx context.Context, req *ledgerv1.TransferRe
logger = logger.With(zap.String("to_role", role.String()))
}
}
s.logLedgerOperation("transfer", logger, resp, err)
s.logLedgerOperationStart(discovery.OperationLedgerTransfer, logger)
responder := s.transferResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError(journalEntryTypeTransfer, journalEntryErrorFailed)
}
s.logLedgerOperation(discovery.OperationLedgerTransfer, logger, resp, err, time.Since(start))
return resp, err
}
@@ -307,17 +317,10 @@ func (s *Service) TransferInternal(ctx context.Context, req *ledgerv1.TransferRe
func (s *Service) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
start := time.Now()
defer func() {
recordJournalEntry("fx", "attempted", time.Since(start).Seconds())
recordJournalEntry(journalEntryTypeFX, journalEntryStatusAttempted, time.Since(start).Seconds())
}()
responder := s.fxResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError("fx", "failed")
}
logger := s.logger.With(zap.String("operation", "fx"))
logger := s.logger.With(zap.String("operation", discovery.OperationLedgerFX))
if req != nil {
logger = logger.With(
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
@@ -341,7 +344,16 @@ func (s *Service) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXReques
logger = logger.With(zap.String("rate", rate))
}
}
s.logLedgerOperation("fx", logger, resp, err)
s.logLedgerOperationStart(discovery.OperationLedgerFX, logger)
responder := s.fxResponder(ctx, req)
resp, err := responder(ctx)
if err != nil {
recordJournalEntryError(journalEntryTypeFX, journalEntryErrorFailed)
}
s.logLedgerOperation(discovery.OperationLedgerFX, logger, resp, err, time.Since(start))
return resp, err
}
@@ -365,23 +377,42 @@ func (s *Service) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryReq
return responder(ctx)
}
func (s *Service) logLedgerOperation(op string, logger *zap.Logger, resp *ledgerv1.PostResponse, err error) {
func (s *Service) logLedgerOperationStart(op string, logger mlogger.Logger) {
if logger == nil {
return
}
if err != nil {
logger.Warn(fmt.Sprintf("ledger %s failed", op), zap.Error(err))
logger.Debug("Ledger operation execution started", zap.String("operation_name", op))
}
func (s *Service) logLedgerOperation(op string, logger mlogger.Logger, resp *ledgerv1.PostResponse, err error, duration time.Duration) {
if logger == nil {
return
}
entryRef := ""
if resp != nil {
entryRef = strings.TrimSpace(resp.GetJournalEntryRef())
}
if entryRef == "" {
logger.Info(fmt.Sprintf("ledger %s posted", op))
status := "succeeded"
fields := []zap.Field{
zap.String("operation_name", op),
zap.String("status", status),
zap.Int64("duration_ms", duration.Milliseconds()),
}
if entryRef != "" {
fields = append(fields, zap.String("journal_entry_ref", entryRef))
}
if err != nil {
fields[1] = zap.String("status", "failed")
logger.Debug("Ledger operation execution completed", append(fields, zap.Error(err))...)
logger.Warn("Ledger operation failed", zap.String("operation_name", op), zap.Error(err))
return
}
logger.Info(fmt.Sprintf("ledger %s posted", op), zap.String("journal_entry_ref", entryRef))
logger.Debug("Ledger operation execution completed", fields...)
if entryRef == "" {
logger.Info("Ledger operation posted", zap.String("operation_name", op))
return
}
logger.Info("Ledger operation posted", zap.String("operation_name", op), zap.String("journal_entry_ref", entryRef))
}
func (s *Service) Shutdown() {
@@ -402,7 +433,7 @@ func (s *Service) startDiscoveryAnnouncer() {
}
announce := discovery.Announcement{
Service: "LEDGER",
Operations: []string{"balance.read", "ledger.debit", "ledger.credit", "external.credit", "external.debit"},
Operations: discovery.LedgerServiceOperations(),
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
@@ -428,8 +459,7 @@ func (s *Service) startOutboxReliableProducer() error {
}
s.outbox.producer = reliableProducer
if s.outbox.producer == nil || s.producer == nil {
s.logger.Info("Outbox reliable publisher disabled",
zap.Bool("enabled", settings.Enabled))
s.logger.Info("Outbox reliable publisher disabled", zap.Bool("enabled", settings.Enabled))
return
}
s.logger.Info("Outbox reliable publisher configured",

View File

@@ -31,13 +31,13 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
@@ -47,10 +47,10 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -119,8 +119,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
@@ -196,8 +196,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -225,8 +225,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -23,6 +23,6 @@ func (a *NotificationAPI) onConfirmationCode(ctx context.Context, account *model
a.logger.Warn("Failed to send confirmation code email", zap.Error(err), mzap.Login(account))
return err
}
a.logger.Info("Confirmation code email sent", mzap.Login(account), zap.String("destination", target), zap.String("target", string(purpose)))
a.logger.Info("Confirmation code email sent", mzap.Login(account), mzap.MaskEmail("destination", target), zap.String("target", string(purpose)))
return nil
}

View File

@@ -120,7 +120,7 @@ func CreateAPI(a api.API) (*NotificationAPI, error) {
announce := discovery.Announcement{
Service: "NOTIFICATIONS",
Operations: []string{"notify.send"},
Operations: []string{discovery.OperationNotifySend},
Version: appversion.Create().Short(),
}
p.announcer = discovery.NewAnnouncer(p.logger, a.Register().Producer(), string(mservice.Notifications), announce)

View File

@@ -30,13 +30,14 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -44,9 +45,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,14 +113,16 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
@@ -179,8 +181,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -208,8 +210,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -53,8 +53,8 @@ func (i *Imp) startDiscoveryAnnouncer(cfg *config, producer msg.Producer) {
announce := discovery.Announcement{
Service: "PAYMENTS_METHODS",
Operations: []string{
"payment_methods.manage",
"payment_methods.read",
discovery.OperationPaymentMethodsManage,
discovery.OperationPaymentMethodsRead,
},
InvokeURI: invokeURI,
Version: appversion.Create().Short(),

View File

@@ -8,8 +8,7 @@ import (
"time"
"github.com/tech/sendico/pkg/merrors"
orchestrationv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
@@ -17,32 +16,23 @@ import (
// Client exposes typed helpers around the payment orchestration and quotation gRPC APIs.
type Client interface {
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error)
GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error)
ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error)
InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error)
ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error)
ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error)
ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error)
ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error)
GetPayment(ctx context.Context, req *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error)
ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error)
Close() error
}
type grpcOrchestratorClient interface {
InitiatePayments(ctx context.Context, in *orchestratorv1.InitiatePaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentsResponse, error)
InitiatePayment(ctx context.Context, in *orchestratorv1.InitiatePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentResponse, error)
CancelPayment(ctx context.Context, in *orchestratorv1.CancelPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.CancelPaymentResponse, error)
GetPayment(ctx context.Context, in *orchestratorv1.GetPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.GetPaymentResponse, error)
ListPayments(ctx context.Context, in *orchestratorv1.ListPaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.ListPaymentsResponse, error)
InitiateConversion(ctx context.Context, in *orchestratorv1.InitiateConversionRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiateConversionResponse, error)
ProcessTransferUpdate(ctx context.Context, in *orchestratorv1.ProcessTransferUpdateRequest, opts ...grpc.CallOption) (*orchestratorv1.ProcessTransferUpdateResponse, error)
ProcessDepositObserved(ctx context.Context, in *orchestratorv1.ProcessDepositObservedRequest, opts ...grpc.CallOption) (*orchestratorv1.ProcessDepositObservedResponse, error)
ExecutePayment(ctx context.Context, in *orchestrationv2.ExecutePaymentRequest, opts ...grpc.CallOption) (*orchestrationv2.ExecutePaymentResponse, error)
ExecuteBatchPayment(ctx context.Context, in *orchestrationv2.ExecuteBatchPaymentRequest, opts ...grpc.CallOption) (*orchestrationv2.ExecuteBatchPaymentResponse, error)
GetPayment(ctx context.Context, in *orchestrationv2.GetPaymentRequest, opts ...grpc.CallOption) (*orchestrationv2.GetPaymentResponse, error)
ListPayments(ctx context.Context, in *orchestrationv2.ListPaymentsRequest, opts ...grpc.CallOption) (*orchestrationv2.ListPaymentsResponse, error)
}
type orchestratorClient struct {
cfg Config
conn *grpc.ClientConn
quoteConn *grpc.ClientConn
client grpcOrchestratorClient
}
@@ -52,29 +42,16 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
if strings.TrimSpace(cfg.Address) == "" {
return nil, merrors.InvalidArgument("payment-orchestrator: address is required")
}
if strings.TrimSpace(cfg.QuoteAddress) == "" {
cfg.QuoteAddress = cfg.Address
}
conn, err := dial(ctx, cfg, cfg.Address, opts...)
if err != nil {
return nil, err
}
quoteConn := conn
if cfg.QuoteAddress != cfg.Address {
quoteConn, err = dial(ctx, cfg, cfg.QuoteAddress, opts...)
if err != nil {
_ = conn.Close()
return nil, err
}
}
return &orchestratorClient{
cfg: cfg,
conn: conn,
quoteConn: quoteConn,
client: orchestrationv1.NewPaymentExecutionServiceClient(conn),
client: orchestrationv2.NewPaymentOrchestratorServiceClient(conn),
}, nil
}
@@ -99,11 +76,6 @@ func dial(ctx context.Context, cfg Config, address string, opts ...grpc.DialOpti
// NewWithClient injects a pre-built orchestrator client (useful for tests).
func NewWithClient(cfg Config, oc grpcOrchestratorClient) Client {
return NewWithClients(cfg, oc)
}
// NewWithClients injects pre-built orchestrator and quotation clients (useful for tests).
func NewWithClients(cfg Config, oc grpcOrchestratorClient) Client {
cfg.setDefaults()
return &orchestratorClient{
cfg: cfg,
@@ -111,69 +83,42 @@ func NewWithClients(cfg Config, oc grpcOrchestratorClient) Client {
}
}
// NewWithClients injects pre-built orchestrator and quotation clients (useful for tests).
func NewWithClients(cfg Config, oc grpcOrchestratorClient) Client {
return NewWithClient(cfg, oc)
}
func (c *orchestratorClient) Close() error {
var firstErr error
if c.quoteConn != nil && c.quoteConn != c.conn {
if err := c.quoteConn.Close(); err != nil {
firstErr = err
if c == nil || c.conn == nil {
return nil
}
}
if c.conn != nil {
if err := c.conn.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
return c.conn.Close()
}
func (c *orchestratorClient) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) {
func (c *orchestratorClient) ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.InitiatePayments(ctx, req)
return c.client.ExecutePayment(ctx, req)
}
func (c *orchestratorClient) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
func (c *orchestratorClient) ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.InitiatePayment(ctx, req)
return c.client.ExecuteBatchPayment(ctx, req)
}
func (c *orchestratorClient) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.CancelPayment(ctx, req)
}
func (c *orchestratorClient) GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) {
func (c *orchestratorClient) GetPayment(ctx context.Context, req *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.GetPayment(ctx, req)
}
func (c *orchestratorClient) ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) {
func (c *orchestratorClient) ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.ListPayments(ctx, req)
}
func (c *orchestratorClient) InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.InitiateConversion(ctx, req)
}
func (c *orchestratorClient) ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.ProcessTransferUpdate(ctx, req)
}
func (c *orchestratorClient) ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.ProcessDepositObserved(ctx, req)
}
func (c *orchestratorClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
timeout := c.cfg.CallTimeout
if timeout <= 0 {

View File

@@ -5,7 +5,6 @@ import "time"
// Config captures connection settings for the payment orchestrator gRPC service.
type Config struct {
Address string
QuoteAddress string
DialTimeout time.Duration
CallTimeout time.Duration
Insecure bool

View File

@@ -3,76 +3,44 @@ package client
import (
"context"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
)
// Fake implements Client for tests.
type Fake struct {
InitiatePaymentsFn func(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
InitiatePaymentFn func(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
CancelPaymentFn func(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error)
GetPaymentFn func(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error)
ListPaymentsFn func(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error)
InitiateConversionFn func(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error)
ProcessTransferUpdateFn func(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error)
ProcessDepositObservedFn func(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error)
ExecutePaymentFn func(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error)
ExecuteBatchPaymentFn func(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error)
GetPaymentFn func(ctx context.Context, req *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error)
ListPaymentsFn func(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error)
CloseFn func() error
}
func (f *Fake) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) {
if f.InitiatePaymentsFn != nil {
return f.InitiatePaymentsFn(ctx, req)
func (f *Fake) ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error) {
if f.ExecutePaymentFn != nil {
return f.ExecutePaymentFn(ctx, req)
}
return &orchestratorv1.InitiatePaymentsResponse{}, nil
return &orchestrationv2.ExecutePaymentResponse{}, nil
}
func (f *Fake) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
if f.InitiatePaymentFn != nil {
return f.InitiatePaymentFn(ctx, req)
func (f *Fake) ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error) {
if f.ExecuteBatchPaymentFn != nil {
return f.ExecuteBatchPaymentFn(ctx, req)
}
return &orchestratorv1.InitiatePaymentResponse{}, nil
return &orchestrationv2.ExecuteBatchPaymentResponse{}, nil
}
func (f *Fake) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) {
if f.CancelPaymentFn != nil {
return f.CancelPaymentFn(ctx, req)
}
return &orchestratorv1.CancelPaymentResponse{}, nil
}
func (f *Fake) GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) {
func (f *Fake) GetPayment(ctx context.Context, req *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error) {
if f.GetPaymentFn != nil {
return f.GetPaymentFn(ctx, req)
}
return &orchestratorv1.GetPaymentResponse{}, nil
return &orchestrationv2.GetPaymentResponse{}, nil
}
func (f *Fake) ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) {
func (f *Fake) ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error) {
if f.ListPaymentsFn != nil {
return f.ListPaymentsFn(ctx, req)
}
return &orchestratorv1.ListPaymentsResponse{}, nil
}
func (f *Fake) InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) {
if f.InitiateConversionFn != nil {
return f.InitiateConversionFn(ctx, req)
}
return &orchestratorv1.InitiateConversionResponse{}, nil
}
func (f *Fake) ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) {
if f.ProcessTransferUpdateFn != nil {
return f.ProcessTransferUpdateFn(ctx, req)
}
return &orchestratorv1.ProcessTransferUpdateResponse{}, nil
}
func (f *Fake) ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) {
if f.ProcessDepositObservedFn != nil {
return f.ProcessDepositObservedFn(ctx, req)
}
return &orchestratorv1.ProcessDepositObservedResponse{}, nil
return &orchestrationv2.ListPaymentsResponse{}, nil
}
func (f *Fake) Close() error {

View File

@@ -38,15 +38,10 @@ messaging:
# Retain quote records after expiry to allow long-running payments to complete.
quote_retention_hours: 72
max_fx_quote_ttl_ms: 600000
# Service endpoints are sourced from discovery; no static overrides.
card_gateways:
monetix:
mcards:
funding_address: "TUaWaCkiXwYPKm5qjcB27Lhwv976vPvedE"
fee_wallet_ref: "697a062a248dc785125ccb9e"
fee_ledger_accounts:
monetix: "697a15cc72e95c92d4c5db01"
# Gateway instances and capabilities are sourced from service discovery.

View File

@@ -38,15 +38,10 @@ messaging:
# Retain quote records after expiry to allow long-running payments to complete.
quote_retention_hours: 72
max_fx_quote_ttl_ms: 600000
# Service endpoints are sourced from discovery; no static overrides.
card_gateways:
monetix:
mcards:
funding_address: "TGBDXEg9rxSqGFJDcb889zqTjDwx1bmLRF"
fee_wallet_ref: "694c124ed76f9f811ac57133"
fee_ledger_accounts:
monetix: "ledger:fees:monetix"
# Gateway instances and capabilities are sourced from service discovery.

View File

@@ -18,7 +18,6 @@ replace github.com/tech/sendico/payments/storage => ../storage
require (
github.com/google/uuid v1.6.0
github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0
github.com/tech/sendico/fx/oracle v0.0.0-00010101000000-000000000000
github.com/tech/sendico/gateway/chain v0.0.0-00010101000000-000000000000
@@ -46,12 +45,13 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
@@ -60,9 +60,9 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.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/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
@@ -182,8 +182,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
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=
@@ -211,8 +211,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -27,22 +27,6 @@ func buildCardGatewayRoutes(src map[string]cardGatewayRouteConfig) map[string]or
return result
}
func buildFeeLedgerAccounts(src map[string]string) map[string]string {
if len(src) == 0 {
return nil
}
result := make(map[string]string, len(src))
for key, account := range src {
k := strings.ToLower(strings.TrimSpace(key))
v := strings.TrimSpace(account)
if k == "" || v == "" {
continue
}
result[k] = v
}
return result
}
func buildGatewayRegistry(logger mlogger.Logger, src []gatewayInstanceConfig, registry *discovery.Registry) orchestrator.GatewayRegistry {
if logger != nil {
logger = logger.Named("gateway_registry")

View File

@@ -13,24 +13,9 @@ import (
type config struct {
*grpcapp.Config `yaml:",inline"`
Fees clientConfig `yaml:"fees"`
Ledger clientConfig `yaml:"ledger"`
Gateway clientConfig `yaml:"gateway"`
PaymentGateway clientConfig `yaml:"payment_gateway"`
Mntx clientConfig `yaml:"mntx"`
Oracle clientConfig `yaml:"oracle"`
CardGateways map[string]cardGatewayRouteConfig `yaml:"card_gateways"`
FeeAccounts map[string]string `yaml:"fee_ledger_accounts"`
GatewayInstances []gatewayInstanceConfig `yaml:"gateway_instances"`
QuoteRetentionHrs int `yaml:"quote_retention_hours"`
MaxFXQuoteTTLMs int64 `yaml:"max_fx_quote_ttl_ms"`
}
type clientConfig struct {
Address string `yaml:"address"`
DialTimeoutSecs int `yaml:"dial_timeout_seconds"`
CallTimeoutSecs int `yaml:"call_timeout_seconds"`
InsecureTransport bool `yaml:"insecure"`
}
type cardGatewayRouteConfig struct {
@@ -79,18 +64,6 @@ type limitsOverrideCfg struct {
MaxOps int `yaml:"max_ops"`
}
const (
defaultMaxFXQuoteTTL = 10 * time.Minute
defaultMaxFXQuoteTTLMillis = int64(defaultMaxFXQuoteTTL / time.Millisecond)
)
func (c clientConfig) callTimeout() time.Duration {
if c.CallTimeoutSecs <= 0 {
return 3 * time.Second
}
return time.Duration(c.CallTimeoutSecs) * time.Second
}
func (c *config) quoteRetention() time.Duration {
if c == nil || c.QuoteRetentionHrs <= 0 {
return 72 * time.Hour
@@ -98,13 +71,6 @@ func (c *config) quoteRetention() time.Duration {
return time.Duration(c.QuoteRetentionHrs) * time.Hour
}
func (c *config) maxFXQuoteTTLMillis() int64 {
if c == nil || c.MaxFXQuoteTTLMs <= 0 {
return defaultMaxFXQuoteTTLMillis
}
return c.MaxFXQuoteTTLMs
}
func (i *Imp) loadConfig() (*config, error) {
data, err := os.ReadFile(i.file)
if err != nil {

View File

@@ -1,20 +1,14 @@
package serverimp
import (
oracleclient "github.com/tech/sendico/fx/oracle/client"
mntxclient "github.com/tech/sendico/gateway/mntx/client"
ledgerclient "github.com/tech/sendico/ledger/client"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
)
type orchestratorDeps struct {
feesClient feesv1.FeeEngineClient
ledgerClient ledgerclient.Client
mntxClient mntxclient.Client
oracleClient oracleclient.Client
quotationClient quotationv1.QuotationServiceClient
gatewayInvokeResolver orchestrator.GatewayInvokeResolver
}
@@ -28,11 +22,8 @@ func (i *Imp) initDependencies(_ *config) *orchestratorDeps {
}
i.discoveryClients = newDiscoveryClientResolver(i.logger, i.discoveryReg)
deps.feesClient = &discoveryFeeClient{resolver: i.discoveryClients}
deps.ledgerClient = &discoveryLedgerClient{resolver: i.discoveryClients}
deps.oracleClient = &discoveryOracleClient{resolver: i.discoveryClients}
deps.mntxClient = &discoveryMntxClient{resolver: i.discoveryClients}
deps.quotationClient = &discoveryQuotationClient{resolver: i.discoveryClients}
deps.gatewayInvokeResolver = discoveryGatewayInvokeResolver{resolver: i.discoveryClients}
return deps
}
@@ -42,9 +33,6 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
return nil
}
opts := []orchestrator.Option{}
if deps.feesClient != nil {
opts = append(opts, orchestrator.WithFeeEngine(deps.feesClient, cfg.Fees.callTimeout()))
}
if deps.ledgerClient != nil {
opts = append(opts, orchestrator.WithLedgerClient(deps.ledgerClient))
}
@@ -52,19 +40,12 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
opts = append(opts, orchestrator.WithMntxGateway(deps.mntxClient))
}
if deps.quotationClient != nil {
opts = append(opts, orchestrator.WithQuotationService(deps.quotationClient))
}
opts = append(opts, orchestrator.WithMaxFXQuoteTTLMillis(cfg.maxFXQuoteTTLMillis()))
if deps.gatewayInvokeResolver != nil {
opts = append(opts, orchestrator.WithGatewayInvokeResolver(deps.gatewayInvokeResolver))
}
if routes := buildCardGatewayRoutes(cfg.CardGateways); len(routes) > 0 {
opts = append(opts, orchestrator.WithCardGatewayRoutes(routes))
}
if feeAccounts := buildFeeLedgerAccounts(cfg.FeeAccounts); len(feeAccounts) > 0 {
opts = append(opts, orchestrator.WithFeeLedgerAccounts(feeAccounts))
}
if registry := buildGatewayRegistry(i.logger, cfg.GatewayInstances, i.discoveryReg); registry != nil {
opts = append(opts, orchestrator.WithGatewayRegistry(registry))
}

View File

@@ -33,7 +33,7 @@ func (i *Imp) initDiscovery(cfg *config) {
}
announce := discovery.Announcement{
Service: "PAYMENTS_ORCHESTRATOR",
Operations: []string{"payment.initiate"},
Operations: []string{discovery.OperationPaymentInitiate},
InvokeURI: cfg.GRPC.DiscoveryInvokeURI(),
Version: appversion.Create().Short(),
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
@@ -32,8 +31,12 @@ var (
feesServiceNames = []string{"BILLING_FEES", string(mservice.FeePlans)}
ledgerServiceNames = []string{"LEDGER", string(mservice.Ledger)}
oracleServiceNames = []string{"FX_ORACLE", string(mservice.FXOracle)}
mntxServiceNames = []string{"CARD_PAYOUT_RAIL_GATEWAY", string(mservice.MntxGateway)}
quoteServiceNames = []string{"PAYMENT_QUOTATION", "payment_quotation"}
mntxServiceNames = []string{"CARD_RAIL_GATEWAY", string(mservice.MntxGateway)}
feesRequiredOps = []string{discovery.OperationFeeCalc}
ledgerRequiredOps = discovery.LedgerServiceOperations()
oracleRequiredOps = []string{discovery.OperationFXQuote}
mntxRequiredOps = discovery.CardPayoutRailGatewayOperations()
)
type discoveryEndpoint struct {
@@ -55,9 +58,6 @@ type discoveryClientResolver struct {
feesConn *grpc.ClientConn
feesEndpoint discoveryEndpoint
quoteConn *grpc.ClientConn
quoteEndpoint discoveryEndpoint
ledgerClient ledgerclient.Client
ledgerEndpoint discoveryEndpoint
@@ -93,10 +93,6 @@ func (r *discoveryClientResolver) Close() {
_ = r.feesConn.Close()
r.feesConn = nil
}
if r.quoteConn != nil {
_ = r.quoteConn.Close()
r.quoteConn = nil
}
if r.ledgerClient != nil {
_ = r.ledgerClient.Close()
r.ledgerClient = nil
@@ -118,32 +114,27 @@ func (r *discoveryClientResolver) Close() {
}
func (r *discoveryClientResolver) FeesAvailable() bool {
_, ok := r.findEntry("fees", feesServiceNames, "", "")
_, ok := r.findEntry("fees", feesServiceNames, "", "", feesRequiredOps)
return ok
}
func (r *discoveryClientResolver) LedgerAvailable() bool {
_, ok := r.findEntry("ledger", ledgerServiceNames, "", "")
_, ok := r.findEntry("ledger", ledgerServiceNames, "", "", ledgerRequiredOps)
return ok
}
func (r *discoveryClientResolver) OracleAvailable() bool {
_, ok := r.findEntry("oracle", oracleServiceNames, "", "")
_, ok := r.findEntry("oracle", oracleServiceNames, "", "", oracleRequiredOps)
return ok
}
func (r *discoveryClientResolver) MntxAvailable() bool {
_, ok := r.findEntry("mntx", mntxServiceNames, "", "")
return ok
}
func (r *discoveryClientResolver) QuotationAvailable() bool {
_, ok := r.findEntry("quotation", quoteServiceNames, "", "")
_, ok := r.findEntry("mntx", mntxServiceNames, "", "", mntxRequiredOps)
return ok
}
func (r *discoveryClientResolver) FeesClient(ctx context.Context) (feesv1.FeeEngineClient, error) {
entry, ok := r.findEntry("fees", feesServiceNames, "", "")
entry, ok := r.findEntry("fees", feesServiceNames, "", "", feesRequiredOps)
if !ok {
return nil, merrors.NoData("discovery: fees service unavailable")
}
@@ -173,39 +164,8 @@ func (r *discoveryClientResolver) FeesClient(ctx context.Context) (feesv1.FeeEng
return feesv1.NewFeeEngineClient(r.feesConn), nil
}
func (r *discoveryClientResolver) QuotationClient(ctx context.Context) (quotationv1.QuotationServiceClient, error) {
entry, ok := r.findEntry("quotation", quoteServiceNames, "", "")
if !ok {
return nil, merrors.NoData("discovery: quotation service unavailable")
}
endpoint, err := parseDiscoveryEndpoint(entry.InvokeURI)
if err != nil {
r.logMissing("quotation", "invalid quotation invoke uri", entry.InvokeURI, err)
return nil, err
}
r.mu.Lock()
defer r.mu.Unlock()
if r.quoteConn == nil || r.quoteEndpoint.key() != endpoint.key() || r.quoteEndpoint.address != endpoint.address {
if r.quoteConn != nil {
_ = r.quoteConn.Close()
r.quoteConn = nil
}
conn, dialErr := dialGrpc(ctx, endpoint)
if dialErr != nil {
r.logMissing("quotation", "failed to dial quotation service", endpoint.raw, dialErr)
return nil, dialErr
}
r.quoteConn = conn
r.quoteEndpoint = endpoint
}
return quotationv1.NewQuotationServiceClient(r.quoteConn), nil
}
func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclient.Client, error) {
entry, ok := r.findEntry("ledger", ledgerServiceNames, "", "")
entry, ok := r.findEntry("ledger", ledgerServiceNames, "", "", ledgerRequiredOps)
if !ok {
return nil, merrors.NoData("discovery: ledger service unavailable")
}
@@ -239,7 +199,7 @@ func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclien
}
func (r *discoveryClientResolver) OracleClient(ctx context.Context) (oracleclient.Client, error) {
entry, ok := r.findEntry("oracle", oracleServiceNames, "", "")
entry, ok := r.findEntry("oracle", oracleServiceNames, "", "", oracleRequiredOps)
if !ok {
return nil, merrors.NoData("discovery: oracle service unavailable")
}
@@ -273,7 +233,7 @@ func (r *discoveryClientResolver) OracleClient(ctx context.Context) (oracleclien
}
func (r *discoveryClientResolver) MntxClient(ctx context.Context) (mntxclient.Client, error) {
entry, ok := r.findEntry("mntx", mntxServiceNames, "", "")
entry, ok := r.findEntry("mntx", mntxServiceNames, "", "", mntxRequiredOps)
if !ok {
return nil, merrors.NoData("discovery: mntx service unavailable")
}
@@ -361,14 +321,19 @@ func (r *discoveryClientResolver) PaymentGatewayClient(ctx context.Context, invo
return client, nil
}
func (r *discoveryClientResolver) findEntry(key string, services []string, rail string, network string) (*discovery.RegistryEntry, bool) {
func (r *discoveryClientResolver) findEntry(key string, services []string, rail string, network string, requiredOps []string) (*discovery.RegistryEntry, bool) {
if r == nil || r.registry == nil {
r.logMissing(key, "discovery registry unavailable", "", nil)
return nil, false
}
type discoveryMatch struct {
entry discovery.RegistryEntry
opMatch bool
}
entries := r.registry.List(time.Now(), true)
matches := make([]discovery.RegistryEntry, 0)
matches := make([]discoveryMatch, 0)
for _, entry := range entries {
if !matchesService(entry.Service, services) {
continue
@@ -379,7 +344,10 @@ func (r *discoveryClientResolver) findEntry(key string, services []string, rail
if network != "" && !strings.EqualFold(strings.TrimSpace(entry.Network), network) {
continue
}
matches = append(matches, entry)
matches = append(matches, discoveryMatch{
entry: entry,
opMatch: discovery.HasAnyOperation(entry.Operations, requiredOps),
})
}
if len(matches) == 0 {
@@ -388,25 +356,25 @@ func (r *discoveryClientResolver) findEntry(key string, services []string, rail
}
sort.Slice(matches, func(i, j int) bool {
if matches[i].RoutingPriority != matches[j].RoutingPriority {
return matches[i].RoutingPriority > matches[j].RoutingPriority
if matches[i].opMatch != matches[j].opMatch {
return matches[i].opMatch
}
if matches[i].ID != matches[j].ID {
return matches[i].ID < matches[j].ID
if matches[i].entry.RoutingPriority != matches[j].entry.RoutingPriority {
return matches[i].entry.RoutingPriority > matches[j].entry.RoutingPriority
}
return matches[i].InstanceID < matches[j].InstanceID
if matches[i].entry.ID != matches[j].entry.ID {
return matches[i].entry.ID < matches[j].entry.ID
}
return matches[i].entry.InstanceID < matches[j].entry.InstanceID
})
entry := matches[0]
entry := matches[0].entry
entryKey := discoveryEntryKey(entry)
r.logSelection(key, entryKey, entry)
return &entry, true
}
func (r *discoveryClientResolver) logSelection(key, entryKey string, entry discovery.RegistryEntry) {
if r.logger == nil {
return
}
r.mu.Lock()
last := r.lastSelection[key]
if last == entryKey {
@@ -426,9 +394,6 @@ func (r *discoveryClientResolver) logSelection(key, entryKey string, entry disco
}
func (r *discoveryClientResolver) logMissing(key, message, invokeURI string, err error) {
if r.logger == nil {
return
}
now := time.Now()
r.mu.Lock()
last := r.lastMissing[key]

View File

@@ -13,7 +13,6 @@ import (
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
"google.golang.org/grpc"
)
@@ -52,33 +51,6 @@ func (c *discoveryFeeClient) ValidateFeeToken(ctx context.Context, req *feesv1.V
return client.ValidateFeeToken(ctx, req, opts...)
}
type discoveryQuotationClient struct {
resolver *discoveryClientResolver
}
func (c *discoveryQuotationClient) Available() bool {
if c == nil || c.resolver == nil {
return false
}
return c.resolver.QuotationAvailable()
}
func (c *discoveryQuotationClient) QuotePayment(ctx context.Context, req *quotationv1.QuotePaymentRequest, opts ...grpc.CallOption) (*quotationv1.QuotePaymentResponse, error) {
client, err := c.resolver.QuotationClient(ctx)
if err != nil {
return nil, err
}
return client.QuotePayment(ctx, req, opts...)
}
func (c *discoveryQuotationClient) QuotePayments(ctx context.Context, req *quotationv1.QuotePaymentsRequest, opts ...grpc.CallOption) (*quotationv1.QuotePaymentsResponse, error) {
client, err := c.resolver.QuotationClient(ctx)
if err != nil {
return nil, err
}
return client.QuotePayments(ctx, req, opts...)
}
type discoveryLedgerClient struct {
resolver *discoveryClientResolver
}
@@ -162,6 +134,22 @@ func (c *discoveryLedgerClient) PostDebitWithCharges(ctx context.Context, req *l
return client.PostDebitWithCharges(ctx, req)
}
func (c *discoveryLedgerClient) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
client, err := c.resolver.LedgerClient(ctx)
if err != nil {
return nil, err
}
return client.PostExternalCreditWithCharges(ctx, req)
}
func (c *discoveryLedgerClient) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
client, err := c.resolver.LedgerClient(ctx)
if err != nil {
return nil, err
}
return client.PostExternalDebitWithCharges(ctx, req)
}
func (c *discoveryLedgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
client, err := c.resolver.LedgerClient(ctx)
if err != nil {

View File

@@ -58,12 +58,12 @@ func (i *Imp) Start() error {
if broker != nil {
opts = append(opts, orchestrator.WithPaymentGatewayBroker(broker))
}
svc := orchestrator.NewService(logger, repo, opts...)
svc, err := orchestrator.NewService(logger, repo, opts...)
i.service = svc
return svc, nil
return svc, err
}
app, err := grpcapp.NewApp(i.logger, "payments_orchestrator", cfg.Config, i.debug, repoFactory, serviceFactory)
app, err := grpcapp.NewApp(i.logger, "payments.orchestrator", cfg.Config, i.debug, repoFactory, serviceFactory)
if err != nil {
return err
}

View File

@@ -5,8 +5,10 @@ import (
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/mlogger"
pm "github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
// Factory builds initial orchestration-v2 payment aggregates.
@@ -41,58 +43,76 @@ const (
// StepShell defines one initial step telemetry item.
type StepShell struct {
StepRef string
StepCode string
StepRef string `bson:"stepRef" json:"stepRef"`
StepCode string `bson:"stepCode" json:"stepCode"`
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
}
// StepExecution is runtime telemetry for one step.
type StepExecution struct {
StepRef string
StepCode string
State StepState
Attempt uint32
StartedAt *time.Time
CompletedAt *time.Time
FailureCode string
FailureMsg string
ExternalRefs []ExternalRef
StepRef string `bson:"stepRef" json:"stepRef"`
StepCode string `bson:"stepCode" json:"stepCode"`
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
State StepState `bson:"state" json:"state"`
Attempt uint32 `bson:"attempt" json:"attempt"`
StartedAt *time.Time `bson:"startedAt,omitempty" json:"startedAt,omitempty"`
CompletedAt *time.Time `bson:"completedAt,omitempty" json:"completedAt,omitempty"`
FailureCode string `bson:"failureCode,omitempty" json:"failureCode,omitempty"`
FailureMsg string `bson:"failureMsg,omitempty" json:"failureMsg,omitempty"`
ExternalRefs []ExternalRef `bson:"externalRefs,omitempty" json:"externalRefs,omitempty"`
}
// ExternalRef links step execution to an external operation.
type ExternalRef struct {
GatewayInstanceID string
Kind string
Ref string
GatewayInstanceID string `bson:"gatewayInstanceId,omitempty" json:"gatewayInstanceId,omitempty"`
Kind string `bson:"kind" json:"kind"`
Ref string `bson:"ref" json:"ref"`
}
// Input defines payload for creating an initial payment aggregate.
type Input struct {
OrganizationRef bson.ObjectID
IdempotencyKey string
QuotationRef string
ClientPaymentRef string
IntentSnapshot model.PaymentIntent
QuoteSnapshot *model.PaymentQuoteSnapshot
Steps []StepShell
OrganizationRef bson.ObjectID `bson:"organizationRef" json:"organizationRef"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
QuotationRef string `bson:"quotationRef" json:"quotationRef"`
ClientPaymentRef string `bson:"clientPaymentRef,omitempty" json:"clientPaymentRef,omitempty"`
IntentSnapshot model.PaymentIntent `bson:"intentSnapshot" json:"intentSnapshot"`
QuoteSnapshot *model.PaymentQuoteSnapshot `bson:"quoteSnapshot" json:"quoteSnapshot"`
Steps []StepShell `bson:"steps,omitempty" json:"steps,omitempty"`
}
// Payment is orchestration-v2 runtime aggregate.
type Payment struct {
storable.Base
pm.OrganizationBoundBase
PaymentRef string
IdempotencyKey string
QuotationRef string
ClientPaymentRef string
IntentSnapshot model.PaymentIntent
QuoteSnapshot *model.PaymentQuoteSnapshot
State State
Version uint64
StepExecutions []StepExecution
storable.Base `bson:",inline" json:",inline"`
pm.OrganizationBoundBase `bson:",inline" json:",inline"`
PaymentRef string `bson:"paymentRef" json:"paymentRef"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
QuotationRef string `bson:"quotationRef" json:"quotationRef"`
ClientPaymentRef string `bson:"clientPaymentRef,omitempty" json:"clientPaymentRef,omitempty"`
IntentSnapshot model.PaymentIntent `bson:"intentSnapshot" json:"intentSnapshot"`
QuoteSnapshot *model.PaymentQuoteSnapshot `bson:"quoteSnapshot" json:"quoteSnapshot"`
State State `bson:"state" json:"state"`
Version uint64 `bson:"version" json:"version"`
StepExecutions []StepExecution `bson:"stepExecutions,omitempty" json:"stepExecutions,omitempty"`
}
func New() Factory {
// Dependencies configures aggregate factory integrations.
type Dependencies struct {
Logger mlogger.Logger
}
func New(deps ...Dependencies) Factory {
var dep Dependencies
if len(deps) > 0 {
dep = deps[0]
}
logger := dep.Logger
if logger == nil {
logger = zap.NewNop()
}
return &svc{
logger: logger.Named("aggregator"),
now: func() time.Time { return time.Now().UTC() },
newID: func() bson.ObjectID {
return bson.NewObjectID()

View File

@@ -7,18 +7,45 @@ import (
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
pm "github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
const initialVersion uint64 = 1
type svc struct {
logger mlogger.Logger
now func() time.Time
newID func() bson.ObjectID
}
func (s *svc) Create(in Input) (*Payment, error) {
func (s *svc) Create(in Input) (payment *Payment, err error) {
logger := s.logger
logger.Debug("Starting Create",
zap.String("organization_ref", in.OrganizationRef.Hex()),
zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
zap.Int("steps_count", len(in.Steps)),
)
defer func(start time.Time) {
fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
if err != nil {
logger.Warn("Failed to create", append(fields, zap.Error(err))...)
return
}
if payment == nil {
logger.Debug("Completed Create", append(fields, zap.Bool("payment_nil", true))...)
return
}
fields = append(fields,
zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
zap.String("state", string(payment.State)),
zap.Uint64("version", payment.Version),
)
logger.Debug("Completed Create", fields...)
}(time.Now())
if in.OrganizationRef.IsZero() {
return nil, merrors.InvalidArgument("organization_id is required")
}
@@ -67,7 +94,7 @@ func (s *svc) Create(in Input) (*Payment, error) {
now := s.now().UTC()
id := s.newID()
return &Payment{
payment = &Payment{
Base: storable.Base{
ID: id,
CreatedAt: now,
@@ -85,7 +112,8 @@ func (s *svc) Create(in Input) (*Payment, error) {
State: StateCreated,
Version: initialVersion,
StepExecutions: stepExecutions,
}, nil
}
return payment, nil
}
func buildInitialStepTelemetry(shell []StepShell) ([]StepExecution, error) {
@@ -109,10 +137,17 @@ func buildInitialStepTelemetry(shell []StepShell) ([]StepExecution, error) {
if stepCode == "" {
return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].step_code is required")
}
visibility := model.NormalizeReportVisibility(shell[i].ReportVisibility)
if !model.IsValidReportVisibility(visibility) {
return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].report_visibility is invalid")
}
userLabel := strings.TrimSpace(shell[i].UserLabel)
out = append(out, StepExecution{
StepRef: stepRef,
StepCode: stepCode,
ReportVisibility: visibility,
UserLabel: userLabel,
State: StepStatePending,
Attempt: 1,
})

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func TestCreate_OK(t *testing.T) {
@@ -17,6 +18,7 @@ func TestCreate_OK(t *testing.T) {
paymentID := bson.NewObjectID()
factory := &svc{
logger: zap.NewNop(),
now: func() time.Time { return now },
newID: func() bson.ObjectID {
return paymentID
@@ -39,8 +41,8 @@ func TestCreate_OK(t *testing.T) {
IntentSnapshot: intent,
QuoteSnapshot: quote,
Steps: []StepShell{
{StepRef: " s1 ", StepCode: " reserve_funds "},
{StepRef: "s2", StepCode: "submit_gateway"},
{StepRef: " s1 ", StepCode: " reserve_funds ", ReportVisibility: model.ReportVisibilityHidden},
{StepRef: "s2", StepCode: "submit_gateway", ReportVisibility: model.ReportVisibilityUser, UserLabel: " Card payout "},
},
})
if err != nil {
@@ -100,6 +102,15 @@ func TestCreate_OK(t *testing.T) {
if payment.StepExecutions[0].State != StepStatePending || payment.StepExecutions[0].Attempt != 1 {
t.Fatalf("unexpected first step shell state: %+v", payment.StepExecutions[0])
}
if got, want := payment.StepExecutions[0].ReportVisibility, model.ReportVisibilityHidden; got != want {
t.Fatalf("unexpected first step visibility: got=%q want=%q", got, want)
}
if got, want := payment.StepExecutions[1].ReportVisibility, model.ReportVisibilityUser; got != want {
t.Fatalf("unexpected second step visibility: got=%q want=%q", got, want)
}
if got, want := payment.StepExecutions[1].UserLabel, "Card payout"; got != want {
t.Fatalf("unexpected second step user label: got=%q want=%q", got, want)
}
// Verify immutable snapshot semantics by ensuring clones were created.
payment.IntentSnapshot.Ref = "changed"
@@ -231,6 +242,19 @@ func TestCreate_InputValidation(t *testing.T) {
},
},
},
{
name: "step report visibility invalid",
in: Input{
OrganizationRef: bson.NewObjectID(),
IdempotencyKey: "idem-1",
QuotationRef: "quote-1",
IntentSnapshot: model.PaymentIntent{Kind: model.PaymentKindPayout, Amount: testMoney()},
QuoteSnapshot: &model.PaymentQuoteSnapshot{QuoteRef: "quote-1"},
Steps: []StepShell{
{StepRef: "s1", StepCode: "code-1", ReportVisibility: model.ReportVisibility("invalid")},
},
},
},
{
name: "step ref must be unique",
in: Input{

View File

@@ -0,0 +1,296 @@
package batchmeta
import (
"encoding/json"
"strconv"
"strings"
"github.com/tech/sendico/payments/storage/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
const (
// AttrPayoutTargets stores serialized payout target descriptors in intent attributes.
AttrPayoutTargets = "orchestrator.v2.batch_payout_targets"
MetaPayoutTargetRef = "orchestrator.v2.payout_target_ref"
MetaPayoutTargetIndex = "orchestrator.v2.payout_target_index"
MetaAmount = "orchestrator.v2.amount"
MetaCurrency = "orchestrator.v2.currency"
MetaCardPan = "orchestrator.v2.card_pan"
MetaCardToken = "orchestrator.v2.card_token"
MetaCardholder = "orchestrator.v2.cardholder"
MetaCardholderSurname = "orchestrator.v2.cardholder_surname"
MetaCardExpMonth = "orchestrator.v2.card_exp_month"
MetaCardExpYear = "orchestrator.v2.card_exp_year"
MetaCardCountry = "orchestrator.v2.card_country"
MetaCardMaskedPan = "orchestrator.v2.card_masked_pan"
MetaCustomerID = "orchestrator.v2.customer_id"
MetaCustomerFirstName = "orchestrator.v2.customer_first_name"
MetaCustomerMiddleName = "orchestrator.v2.customer_middle_name"
MetaCustomerLastName = "orchestrator.v2.customer_last_name"
MetaCustomerIP = "orchestrator.v2.customer_ip"
MetaCustomerZip = "orchestrator.v2.customer_zip"
MetaCustomerCountry = "orchestrator.v2.customer_country"
MetaCustomerState = "orchestrator.v2.customer_state"
MetaCustomerCity = "orchestrator.v2.customer_city"
MetaCustomerAddress = "orchestrator.v2.customer_address"
)
// PayoutTarget carries one destination-level payout branch payload for batch execution.
type PayoutTarget struct {
TargetRef string `json:"target_ref,omitempty"`
IntentRef []string `json:"intent_refs,omitempty"`
Amount *paymenttypes.Money `json:"amount,omitempty"`
Card *model.CardEndpoint `json:"card,omitempty"`
Customer *model.Customer `json:"customer,omitempty"`
}
func EncodePayoutTargets(targets []PayoutTarget) (string, error) {
norm := normalizeTargets(targets)
if len(norm) == 0 {
return "", nil
}
data, err := json.Marshal(norm)
if err != nil {
return "", err
}
return string(data), nil
}
func DecodePayoutTargets(raw string) ([]PayoutTarget, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
var targets []PayoutTarget
if err := json.Unmarshal([]byte(raw), &targets); err != nil {
return nil, err
}
return normalizeTargets(targets), nil
}
func StepMetadataForTarget(target PayoutTarget, index int) map[string]string {
target = normalizeTarget(target, index)
out := map[string]string{}
if ref := strings.TrimSpace(target.TargetRef); ref != "" {
out[MetaPayoutTargetRef] = ref
}
if index >= 0 {
out[MetaPayoutTargetIndex] = strconv.Itoa(index + 1)
}
if target.Amount != nil {
if amount := strings.TrimSpace(target.Amount.Amount); amount != "" {
out[MetaAmount] = amount
}
if currency := strings.ToUpper(strings.TrimSpace(target.Amount.Currency)); currency != "" {
out[MetaCurrency] = currency
}
}
if target.Card != nil {
appendIfNotEmpty(out, MetaCardPan, target.Card.Pan)
appendIfNotEmpty(out, MetaCardToken, target.Card.Token)
appendIfNotEmpty(out, MetaCardholder, target.Card.Cardholder)
appendIfNotEmpty(out, MetaCardholderSurname, target.Card.CardholderSurname)
appendIfNotEmpty(out, MetaCardCountry, target.Card.Country)
appendIfNotEmpty(out, MetaCardMaskedPan, target.Card.MaskedPan)
if target.Card.ExpMonth != 0 {
out[MetaCardExpMonth] = strconv.FormatUint(uint64(target.Card.ExpMonth), 10)
}
if target.Card.ExpYear != 0 {
out[MetaCardExpYear] = strconv.FormatUint(uint64(target.Card.ExpYear), 10)
}
}
if target.Customer != nil {
appendIfNotEmpty(out, MetaCustomerID, target.Customer.ID)
appendIfNotEmpty(out, MetaCustomerFirstName, target.Customer.FirstName)
appendIfNotEmpty(out, MetaCustomerMiddleName, target.Customer.MiddleName)
appendIfNotEmpty(out, MetaCustomerLastName, target.Customer.LastName)
appendIfNotEmpty(out, MetaCustomerIP, target.Customer.IP)
appendIfNotEmpty(out, MetaCustomerZip, target.Customer.Zip)
appendIfNotEmpty(out, MetaCustomerCountry, target.Customer.Country)
appendIfNotEmpty(out, MetaCustomerState, target.Customer.State)
appendIfNotEmpty(out, MetaCustomerCity, target.Customer.City)
appendIfNotEmpty(out, MetaCustomerAddress, target.Customer.Address)
}
if len(out) == 0 {
return nil
}
return out
}
func AmountFromMetadata(metadata map[string]string) (*paymenttypes.Money, bool) {
if len(metadata) == 0 {
return nil, false
}
amount := strings.TrimSpace(metadata[MetaAmount])
currency := strings.ToUpper(strings.TrimSpace(metadata[MetaCurrency]))
if amount == "" || currency == "" {
return nil, false
}
return &paymenttypes.Money{Amount: amount, Currency: currency}, true
}
func CardFromMetadata(metadata map[string]string) (*model.CardEndpoint, bool) {
if len(metadata) == 0 {
return nil, false
}
card := &model.CardEndpoint{
Pan: strings.TrimSpace(metadata[MetaCardPan]),
Token: strings.TrimSpace(metadata[MetaCardToken]),
Cardholder: strings.TrimSpace(metadata[MetaCardholder]),
CardholderSurname: strings.TrimSpace(metadata[MetaCardholderSurname]),
Country: strings.ToUpper(strings.TrimSpace(metadata[MetaCardCountry])),
MaskedPan: strings.TrimSpace(metadata[MetaCardMaskedPan]),
}
if parsed, ok := parseUint32(metadata[MetaCardExpMonth]); ok {
card.ExpMonth = parsed
}
if parsed, ok := parseUint32(metadata[MetaCardExpYear]); ok {
card.ExpYear = parsed
}
if card.Pan == "" && card.Token == "" && card.Cardholder == "" &&
card.CardholderSurname == "" && card.ExpMonth == 0 && card.ExpYear == 0 &&
card.Country == "" && card.MaskedPan == "" {
return nil, false
}
return card, true
}
func CustomerFromMetadata(metadata map[string]string) (*model.Customer, bool) {
if len(metadata) == 0 {
return nil, false
}
customer := &model.Customer{
ID: strings.TrimSpace(metadata[MetaCustomerID]),
FirstName: strings.TrimSpace(metadata[MetaCustomerFirstName]),
MiddleName: strings.TrimSpace(metadata[MetaCustomerMiddleName]),
LastName: strings.TrimSpace(metadata[MetaCustomerLastName]),
IP: strings.TrimSpace(metadata[MetaCustomerIP]),
Zip: strings.TrimSpace(metadata[MetaCustomerZip]),
Country: strings.ToUpper(strings.TrimSpace(metadata[MetaCustomerCountry])),
State: strings.TrimSpace(metadata[MetaCustomerState]),
City: strings.TrimSpace(metadata[MetaCustomerCity]),
Address: strings.TrimSpace(metadata[MetaCustomerAddress]),
}
if customer.ID == "" && customer.FirstName == "" && customer.MiddleName == "" &&
customer.LastName == "" && customer.IP == "" && customer.Zip == "" &&
customer.Country == "" && customer.State == "" && customer.City == "" &&
customer.Address == "" {
return nil, false
}
return customer, true
}
func normalizeTargets(targets []PayoutTarget) []PayoutTarget {
if len(targets) == 0 {
return nil
}
out := make([]PayoutTarget, 0, len(targets))
for i := range targets {
target := normalizeTarget(targets[i], i)
if target.TargetRef == "" && target.Amount == nil && target.Card == nil && target.Customer == nil {
continue
}
out = append(out, target)
}
if len(out) == 0 {
return nil
}
return out
}
func normalizeTarget(target PayoutTarget, index int) PayoutTarget {
target.TargetRef = strings.TrimSpace(target.TargetRef)
if target.TargetRef == "" {
target.TargetRef = "target-" + strconv.Itoa(index+1)
}
target.IntentRef = normalizeStringSlice(target.IntentRef)
if target.Amount != nil {
amount := strings.TrimSpace(target.Amount.Amount)
currency := strings.ToUpper(strings.TrimSpace(target.Amount.Currency))
if amount == "" || currency == "" {
target.Amount = nil
} else {
target.Amount = &paymenttypes.Money{
Amount: amount,
Currency: currency,
}
}
}
if target.Card != nil {
target.Card = &model.CardEndpoint{
Pan: strings.TrimSpace(target.Card.Pan),
Token: strings.TrimSpace(target.Card.Token),
Cardholder: strings.TrimSpace(target.Card.Cardholder),
CardholderSurname: strings.TrimSpace(target.Card.CardholderSurname),
ExpMonth: target.Card.ExpMonth,
ExpYear: target.Card.ExpYear,
Country: strings.ToUpper(strings.TrimSpace(target.Card.Country)),
MaskedPan: strings.TrimSpace(target.Card.MaskedPan),
}
}
if target.Customer != nil {
target.Customer = &model.Customer{
ID: strings.TrimSpace(target.Customer.ID),
FirstName: strings.TrimSpace(target.Customer.FirstName),
MiddleName: strings.TrimSpace(target.Customer.MiddleName),
LastName: strings.TrimSpace(target.Customer.LastName),
IP: strings.TrimSpace(target.Customer.IP),
Zip: strings.TrimSpace(target.Customer.Zip),
Country: strings.ToUpper(strings.TrimSpace(target.Customer.Country)),
State: strings.TrimSpace(target.Customer.State),
City: strings.TrimSpace(target.Customer.City),
Address: strings.TrimSpace(target.Customer.Address),
}
}
return target
}
func normalizeStringSlice(values []string) []string {
if len(values) == 0 {
return nil
}
seen := make(map[string]struct{}, len(values))
out := make([]string, 0, len(values))
for i := range values {
token := strings.TrimSpace(values[i])
if token == "" {
continue
}
if _, exists := seen[token]; exists {
continue
}
seen[token] = struct{}{}
out = append(out, token)
}
if len(out) == 0 {
return nil
}
return out
}
func appendIfNotEmpty(dst map[string]string, key string, value string) {
if dst == nil {
return
}
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return
}
dst[key] = trimmed
}
func parseUint32(raw string) (uint32, bool) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return 0, false
}
parsed, err := strconv.ParseUint(trimmed, 10, 32)
if err != nil {
return 0, false
}
return uint32(parsed), true
}

View File

@@ -0,0 +1,9 @@
package erecon
import "errors"
var (
ErrStepNotFound = errors.New("step execution not found")
ErrAmbiguousStepMatch = errors.New("ambiguous step execution match")
ErrStepTransitionInvalid = errors.New("step transition invalid")
)

View File

@@ -0,0 +1,326 @@
package erecon
import (
"strings"
"time"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/pkg/merrors"
)
type normalizedEvent struct {
stepRef string `bson:"stepRef"`
matchRefs []agg.ExternalRef `bson:"matchRefs"`
appendRefs []agg.ExternalRef `bson:"appendRefs"`
targetState agg.StepState `bson:"targetState"`
failureInfo *failureInfo `bson:"failure,omitempty"`
forceAggregate *forceAggregate `bson:"forceAggregate,omitempty"`
}
type failureInfo struct {
code string `bson:"code"`
msg string `bson:"message"`
occurredAt *time.Time `bson:"occurredAt,omitempty"`
}
type forceAggregate struct {
failed bool `bson:"failed"`
needsAttention bool `bson:"needsAttention"`
}
func normalizeEvent(event Event) (*normalizedEvent, error) {
if countPayloads(event) != 1 {
return nil, merrors.InvalidArgument("exactly one event payload is required")
}
if event.Gateway != nil {
return normalizeGatewayEvent(*event.Gateway)
}
if event.Ledger != nil {
return normalizeLedgerEvent(*event.Ledger)
}
return normalizeCardEvent(*event.Card)
}
func countPayloads(event Event) int {
count := 0
if event.Gateway != nil {
count++
}
if event.Ledger != nil {
count++
}
if event.Card != nil {
count++
}
return count
}
func (e *normalizedEvent) failureCodeValue() string {
if e == nil || e.failureInfo == nil {
return ""
}
return strings.TrimSpace(e.failureInfo.code)
}
func (e *normalizedEvent) failureMsgValue() string {
if e == nil || e.failureInfo == nil {
return ""
}
return strings.TrimSpace(e.failureInfo.msg)
}
func (e *normalizedEvent) occurredAtValue() *time.Time {
if e == nil || e.failureInfo == nil {
return nil
}
return e.failureInfo.occurredAt
}
func (e *normalizedEvent) forceAggregateFailedValue() bool {
return e != nil && e.forceAggregate != nil && e.forceAggregate.failed
}
func (e *normalizedEvent) forceAggregateNeedsAttentionValue() bool {
return e != nil && e.forceAggregate != nil && e.forceAggregate.needsAttention
}
func buildFailureInfo(code, msg string, occurredAt *time.Time) *failureInfo {
if code == "" && msg == "" && occurredAt == nil {
return nil
}
return &failureInfo{
code: code,
msg: msg,
occurredAt: occurredAt,
}
}
func buildForceAggregate(failed, needsAttention bool) *forceAggregate {
if !failed && !needsAttention {
return nil
}
return &forceAggregate{
failed: failed,
needsAttention: needsAttention,
}
}
func normalizeGatewayEvent(src GatewayEvent) (*normalizedEvent, error) {
status, ok := normalizeGatewayStatus(src.Status)
if !ok {
return nil, merrors.InvalidArgument("gateway status is invalid")
}
target, needsAttention := mapFailureTarget(status, src.Retryable)
failureCode := strings.TrimSpace(src.FailureCode)
failureMsg := strings.TrimSpace(src.FailureMsg)
if target == agg.StepStateFailed && failureMsg == "" {
failureMsg = "gateway operation failed"
}
ev := &normalizedEvent{
stepRef: strings.TrimSpace(src.StepRef),
targetState: target,
failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)),
forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
}
ev.matchRefs = normalizeRefList([]agg.ExternalRef{
{
GatewayInstanceID: strings.TrimSpace(src.GatewayInstanceID),
Kind: ExternalRefKindOperation,
Ref: strings.TrimSpace(src.OperationRef),
},
{
GatewayInstanceID: strings.TrimSpace(src.GatewayInstanceID),
Kind: ExternalRefKindTransfer,
Ref: strings.TrimSpace(src.TransferRef),
},
})
ev.appendRefs = cloneRefs(ev.matchRefs)
if ev.stepRef == "" && len(ev.matchRefs) == 0 {
return nil, merrors.InvalidArgument("gateway event must include step_ref or operation/transfer reference")
}
return ev, nil
}
func normalizeLedgerEvent(src LedgerEvent) (*normalizedEvent, error) {
status, ok := normalizeLedgerStatus(src.Status)
if !ok {
return nil, merrors.InvalidArgument("ledger status is invalid")
}
target, needsAttention := mapFailureTarget(status, src.Retryable)
failureCode := strings.TrimSpace(src.FailureCode)
failureMsg := strings.TrimSpace(src.FailureMsg)
if target == agg.StepStateFailed && failureMsg == "" {
failureMsg = "ledger operation failed"
}
ev := &normalizedEvent{
stepRef: strings.TrimSpace(src.StepRef),
targetState: target,
failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)),
forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
}
ev.matchRefs = normalizeRefList([]agg.ExternalRef{
{
Kind: ExternalRefKindLedger,
Ref: strings.TrimSpace(src.EntryRef),
},
})
ev.appendRefs = cloneRefs(ev.matchRefs)
if ev.stepRef == "" && len(ev.matchRefs) == 0 {
return nil, merrors.InvalidArgument("ledger event must include step_ref or entry_ref")
}
return ev, nil
}
func normalizeCardEvent(src CardEvent) (*normalizedEvent, error) {
status, ok := normalizeCardStatus(src.Status)
if !ok {
return nil, merrors.InvalidArgument("card status is invalid")
}
target, needsAttention := mapFailureTarget(status, src.Retryable)
failureCode := strings.TrimSpace(src.FailureCode)
failureMsg := strings.TrimSpace(src.FailureMsg)
if target == agg.StepStateFailed && failureMsg == "" {
failureMsg = "card payout failed"
}
ev := &normalizedEvent{
stepRef: strings.TrimSpace(src.StepRef),
targetState: target,
failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)),
forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
}
ev.matchRefs = normalizeRefList([]agg.ExternalRef{
{
GatewayInstanceID: strings.TrimSpace(src.GatewayInstanceID),
Kind: ExternalRefKindCardPayout,
Ref: strings.TrimSpace(src.PayoutRef),
},
})
ev.appendRefs = cloneRefs(ev.matchRefs)
if ev.stepRef == "" && len(ev.matchRefs) == 0 {
return nil, merrors.InvalidArgument("card event must include step_ref or payout_ref")
}
return ev, nil
}
func normalizeGatewayStatus(status GatewayStatus) (GatewayStatus, bool) {
switch strings.ToLower(strings.TrimSpace(string(status))) {
case string(GatewayStatusCreated):
return GatewayStatusCreated, true
case string(GatewayStatusProcessing):
return GatewayStatusProcessing, true
case string(GatewayStatusWaiting):
return GatewayStatusWaiting, true
case string(GatewayStatusSuccess):
return GatewayStatusSuccess, true
case string(GatewayStatusFailed):
return GatewayStatusFailed, true
case string(GatewayStatusCancelled):
return GatewayStatusCancelled, true
default:
return GatewayStatusUnspecified, false
}
}
func normalizeLedgerStatus(status LedgerStatus) (LedgerStatus, bool) {
switch strings.ToLower(strings.TrimSpace(string(status))) {
case string(LedgerStatusPending):
return LedgerStatusPending, true
case string(LedgerStatusProcessing):
return LedgerStatusProcessing, true
case string(LedgerStatusPosted):
return LedgerStatusPosted, true
case string(LedgerStatusFailed):
return LedgerStatusFailed, true
case string(LedgerStatusCancelled):
return LedgerStatusCancelled, true
default:
return LedgerStatusUnspecified, false
}
}
func normalizeCardStatus(status CardStatus) (CardStatus, bool) {
switch strings.ToLower(strings.TrimSpace(string(status))) {
case string(CardStatusCreated):
return CardStatusCreated, true
case string(CardStatusProcessing):
return CardStatusProcessing, true
case string(CardStatusWaiting):
return CardStatusWaiting, true
case string(CardStatusSuccess):
return CardStatusSuccess, true
case string(CardStatusFailed):
return CardStatusFailed, true
case string(CardStatusCancelled):
return CardStatusCancelled, true
default:
return CardStatusUnspecified, false
}
}
func mapFailureTarget(status any, retryable *bool) (agg.StepState, bool) {
switch status {
case GatewayStatusCreated, GatewayStatusProcessing, GatewayStatusWaiting:
return agg.StepStateRunning, false
case LedgerStatusPending, LedgerStatusProcessing:
return agg.StepStateRunning, false
case CardStatusCreated, CardStatusProcessing, CardStatusWaiting:
return agg.StepStateRunning, false
case GatewayStatusSuccess, LedgerStatusPosted, CardStatusSuccess:
return agg.StepStateCompleted, false
case GatewayStatusFailed, GatewayStatusCancelled, LedgerStatusFailed, LedgerStatusCancelled, CardStatusFailed, CardStatusCancelled:
if retryable != nil && !*retryable {
return agg.StepStateNeedsAttention, true
}
return agg.StepStateFailed, false
default:
return agg.StepStateUnspecified, false
}
}
func normalizeTimePtr(ts *time.Time) *time.Time {
if ts == nil {
return nil
}
val := ts.UTC()
return &val
}
func normalizeRefList(refs []agg.ExternalRef) []agg.ExternalRef {
if len(refs) == 0 {
return nil
}
out := make([]agg.ExternalRef, 0, len(refs))
seen := map[string]struct{}{}
for i := range refs {
ref := refs[i]
ref.GatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID)
ref.Kind = strings.TrimSpace(ref.Kind)
ref.Ref = strings.TrimSpace(ref.Ref)
if ref.Kind == "" || ref.Ref == "" {
continue
}
key := ref.GatewayInstanceID + "\x1f" + strings.ToLower(ref.Kind) + "\x1f" + strings.ToLower(ref.Ref)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, ref)
}
return out
}
func cloneRefs(refs []agg.ExternalRef) []agg.ExternalRef {
if len(refs) == 0 {
return nil
}
out := make([]agg.ExternalRef, 0, len(refs))
out = append(out, refs...)
return out
}

View File

@@ -0,0 +1,97 @@
package erecon
import (
"strings"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xerr"
)
func findStepIndex(payment *agg.Payment, event *normalizedEvent) (int, error) {
if payment == nil {
return -1, ErrStepNotFound
}
if event == nil {
return -1, ErrStepNotFound
}
if stepRef := strings.TrimSpace(event.stepRef); stepRef != "" {
for i := range payment.StepExecutions {
if strings.EqualFold(strings.TrimSpace(payment.StepExecutions[i].StepRef), stepRef) {
return i, nil
}
}
return -1, xerr.Wrapf(ErrStepNotFound, "step_ref=%s", stepRef)
}
matches := make([]int, 0, 1)
for i := range payment.StepExecutions {
if stepMatchesAnyRef(payment.StepExecutions[i], event.matchRefs) {
matches = append(matches, i)
}
}
switch len(matches) {
case 0:
return -1, ErrStepNotFound
case 1:
return matches[0], nil
default:
return -1, ErrAmbiguousStepMatch
}
}
func stepMatchesAnyRef(step agg.StepExecution, refs []agg.ExternalRef) bool {
if len(refs) == 0 || len(step.ExternalRefs) == 0 {
return false
}
for i := range refs {
if hasExternalRef(step.ExternalRefs, refs[i]) {
return true
}
}
return false
}
func hasExternalRef(existing []agg.ExternalRef, ref agg.ExternalRef) bool {
kind := strings.TrimSpace(ref.Kind)
value := strings.TrimSpace(ref.Ref)
gatewayID := strings.TrimSpace(ref.GatewayInstanceID)
if kind == "" || value == "" {
return false
}
for i := range existing {
candidate := existing[i]
if !strings.EqualFold(strings.TrimSpace(candidate.Kind), kind) {
continue
}
if !strings.EqualFold(strings.TrimSpace(candidate.Ref), value) {
continue
}
if gatewayID != "" && strings.TrimSpace(candidate.GatewayInstanceID) != "" && !strings.EqualFold(strings.TrimSpace(candidate.GatewayInstanceID), gatewayID) {
continue
}
return true
}
return false
}
func mergeExternalRefs(existing []agg.ExternalRef, additions []agg.ExternalRef) ([]agg.ExternalRef, bool) {
if len(additions) == 0 {
return cloneRefs(existing), false
}
out := cloneRefs(existing)
changed := false
for i := range additions {
ref := additions[i]
if hasExternalRef(out, ref) {
continue
}
out = append(out, agg.ExternalRef{
GatewayInstanceID: strings.TrimSpace(ref.GatewayInstanceID),
Kind: strings.TrimSpace(ref.Kind),
Ref: strings.TrimSpace(ref.Ref),
})
changed = true
}
return out, changed
}

View File

@@ -0,0 +1,146 @@
package erecon
import (
"time"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
// Reconciler applies external async events to payment runtime state.
type Reconciler interface {
Reconcile(in Input) (*Output, error)
}
// Input is the reconciliation payload.
type Input struct {
Payment *agg.Payment
Event Event
}
// Output is the reconciliation result.
type Output struct {
Payment *agg.Payment
MatchedStepRef string
StepChanged bool
AggregateChanged bool
}
// Event is one transport-agnostic external event envelope.
// Exactly one payload must be set.
type Event struct {
Gateway *GatewayEvent
Ledger *LedgerEvent
Card *CardEvent
}
// GatewayStatus is gateway operation lifecycle status.
type GatewayStatus string
const (
GatewayStatusUnspecified GatewayStatus = "unspecified"
GatewayStatusCreated GatewayStatus = "created"
GatewayStatusProcessing GatewayStatus = "processing"
GatewayStatusWaiting GatewayStatus = "waiting"
GatewayStatusSuccess GatewayStatus = "success"
GatewayStatusFailed GatewayStatus = "failed"
GatewayStatusCancelled GatewayStatus = "cancelled"
)
// LedgerStatus is ledger operation lifecycle status.
type LedgerStatus string
const (
LedgerStatusUnspecified LedgerStatus = "unspecified"
LedgerStatusPending LedgerStatus = "pending"
LedgerStatusProcessing LedgerStatus = "processing"
LedgerStatusPosted LedgerStatus = "posted"
LedgerStatusFailed LedgerStatus = "failed"
LedgerStatusCancelled LedgerStatus = "cancelled"
)
// CardStatus is card payout lifecycle status.
type CardStatus string
const (
CardStatusUnspecified CardStatus = "unspecified"
CardStatusCreated CardStatus = "created"
CardStatusProcessing CardStatus = "processing"
CardStatusWaiting CardStatus = "waiting"
CardStatusSuccess CardStatus = "success"
CardStatusFailed CardStatus = "failed"
CardStatusCancelled CardStatus = "cancelled"
)
// GatewayEvent is one async event from gateway execution flow.
type GatewayEvent struct {
StepRef string
OperationRef string
TransferRef string
GatewayInstanceID string
Status GatewayStatus
FailureCode string
FailureMsg string
Retryable *bool
TerminalFailure bool
OccurredAt *time.Time
}
// LedgerEvent is one async event from ledger flow.
type LedgerEvent struct {
StepRef string
EntryRef string
Status LedgerStatus
FailureCode string
FailureMsg string
Retryable *bool
TerminalFailure bool
OccurredAt *time.Time
}
// CardEvent is one async event from card payout flow.
type CardEvent struct {
StepRef string
PayoutRef string
GatewayInstanceID string
Status CardStatus
FailureCode string
FailureMsg string
Retryable *bool
TerminalFailure bool
OccurredAt *time.Time
}
const (
ExternalRefKindOperation = "operation_ref"
ExternalRefKindTransfer = "transfer_ref"
ExternalRefKindLedger = "ledger_entry_ref"
ExternalRefKindCardPayout = "card_payout_ref"
)
// Dependencies configures reconciliation service integrations.
type Dependencies struct {
Logger mlogger.Logger
Now func() time.Time
}
func New(deps ...Dependencies) Reconciler {
var dep Dependencies
if len(deps) > 0 {
dep = deps[0]
}
logger := dep.Logger
if logger == nil {
logger = zap.NewNop()
}
now := dep.Now
if now == nil {
now = defaultNow
}
return &svc{
logger: logger.Named("reconciler"),
now: now,
}
}

View File

@@ -0,0 +1,119 @@
package erecon
import (
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ostate"
)
func reduceAggregateState(payment *agg.Payment, event *normalizedEvent, sm ostate.StateMachine) (bool, error) {
if payment == nil || sm == nil {
return false, nil
}
target := deriveAggregateTarget(payment, event, sm)
return applyAggregateTarget(payment, target, sm)
}
func deriveAggregateTarget(payment *agg.Payment, event *normalizedEvent, sm ostate.StateMachine) agg.State {
if payment == nil {
return agg.StateUnspecified
}
if event != nil && event.forceAggregateFailedValue() {
return agg.StateFailed
}
hasNeedsAttention := false
hasWork := false
allTerminalSuccessOrSkipped := len(payment.StepExecutions) > 0
for i := range payment.StepExecutions {
state := payment.StepExecutions[i].State
switch state {
case agg.StepStateCompleted, agg.StepStateSkipped:
hasWork = true
case agg.StepStatePending, agg.StepStateUnspecified:
allTerminalSuccessOrSkipped = false
case agg.StepStateNeedsAttention:
hasWork = true
hasNeedsAttention = true
allTerminalSuccessOrSkipped = false
case agg.StepStateRunning, agg.StepStateFailed:
hasWork = true
allTerminalSuccessOrSkipped = false
default:
allTerminalSuccessOrSkipped = false
}
}
if allTerminalSuccessOrSkipped {
return agg.StateSettled
}
if hasNeedsAttention || (event != nil && event.forceAggregateNeedsAttentionValue()) {
return agg.StateNeedsAttention
}
if hasWork {
return agg.StateExecuting
}
if sm.IsAggregateTerminal(payment.State) {
return payment.State
}
return agg.StateCreated
}
func applyAggregateTarget(payment *agg.Payment, target agg.State, sm ostate.StateMachine) (bool, error) {
if payment == nil || sm == nil {
return false, nil
}
current := payment.State
if current == target {
return false, nil
}
if sm.IsAggregateTerminal(current) {
return false, nil
}
original := current
for i := 0; i < 6 && current != target; i++ {
if sm.EnsureAggregateTransition(current, target) == nil {
current = target
break
}
next, ok := nextAggregateHop(current, target)
if !ok {
break
}
if sm.EnsureAggregateTransition(current, next) != nil {
break
}
current = next
}
if current != target {
return false, nil
}
payment.State = current
return payment.State != original, nil
}
func nextAggregateHop(current, target agg.State) (agg.State, bool) {
switch current {
case agg.StateUnspecified:
return agg.StateCreated, true
case agg.StateCreated:
if target == agg.StateFailed {
return agg.StateFailed, true
}
if target == agg.StateExecuting {
return agg.StateExecuting, true
}
return agg.StateExecuting, true
case agg.StateExecuting:
return target, true
case agg.StateNeedsAttention:
if target == agg.StateCreated {
return agg.StateExecuting, true
}
return target, true
default:
return agg.StateUnspecified, false
}
}

View File

@@ -0,0 +1,259 @@
package erecon
import (
"strings"
"time"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ostate"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xerr"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
type svc struct {
logger mlogger.Logger
now func() time.Time
}
func defaultNow() time.Time {
return time.Now().UTC()
}
func (s *svc) Reconcile(in Input) (out *Output, err error) {
logger := s.logger
paymentRef := ""
if in.Payment != nil {
paymentRef = strings.TrimSpace(in.Payment.PaymentRef)
}
logger.Debug("Starting Reconcile",
zap.String("payment_ref", paymentRef),
zap.String("event_source", eventSource(in.Event)),
)
defer func(start time.Time) {
fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
if out != nil {
fields = append(fields,
zap.String("matched_step_ref", strings.TrimSpace(out.MatchedStepRef)),
zap.Bool("step_changed", out.StepChanged),
zap.Bool("aggregate_changed", out.AggregateChanged),
)
if out.Payment != nil {
fields = append(fields,
zap.String("payment_state", string(out.Payment.State)),
zap.Uint64("version", out.Payment.Version),
)
}
}
if err != nil {
logger.Warn("Failed to reconcile", append(fields, zap.Error(err))...)
return
}
logger.Debug("Completed Reconcile", fields...)
}(time.Now())
if in.Payment == nil {
return nil, merrors.InvalidArgument("payment is required")
}
if len(in.Payment.StepExecutions) == 0 {
return nil, merrors.InvalidArgument("payment.step_executions are required")
}
event, err := normalizeEvent(in.Event)
if err != nil {
return nil, err
}
payment, err := clonePayment(in.Payment)
if err != nil {
return nil, err
}
idx, err := findStepIndex(payment, event)
if err != nil {
return nil, err
}
sm := ostate.New(ostate.Dependencies{Logger: logger.Named("ostate")})
stepChanged, err := s.applyStepEvent(&payment.StepExecutions[idx], event, sm)
if err != nil {
return nil, err
}
aggregateChanged, err := reduceAggregateState(payment, event, sm)
if err != nil {
return nil, err
}
if stepChanged || aggregateChanged {
payment.Version++
payment.UpdatedAt = s.now().UTC()
}
out = &Output{
Payment: payment,
MatchedStepRef: payment.StepExecutions[idx].StepRef,
StepChanged: stepChanged,
AggregateChanged: aggregateChanged,
}
return out, nil
}
func (s *svc) applyStepEvent(step *agg.StepExecution, event *normalizedEvent, sm ostate.StateMachine) (bool, error) {
if step == nil || event == nil {
return false, nil
}
changed := false
out := *step
refs, refsChanged := mergeExternalRefs(out.ExternalRefs, event.appendRefs)
if refsChanged {
out.ExternalRefs = refs
changed = true
}
target := event.targetState
if target == agg.StepStateUnspecified {
*step = out
return changed, nil
}
if out.State == target {
changed = s.applyStepDiagnostics(&out, event) || changed
*step = out
return changed, nil
}
if sm.IsStepTerminal(out.State) {
*step = out
return changed, nil
}
next, transitionChanged, err := transitionStepState(out, target, sm)
if err != nil {
return false, err
}
out = next
changed = changed || transitionChanged
changed = s.applyStepDiagnostics(&out, event) || changed
*step = out
return changed, nil
}
func transitionStepState(step agg.StepExecution, target agg.StepState, sm ostate.StateMachine) (agg.StepExecution, bool, error) {
if step.State == target {
return step, false, nil
}
if sm.EnsureStepTransition(step.State, target) == nil {
step.State = target
return step, true, nil
}
original := step.State
bridge := []agg.StepState{agg.StepStateRunning, target}
for i := range bridge {
next := bridge[i]
if step.State == next {
continue
}
if sm.EnsureStepTransition(step.State, next) != nil {
return step, false, xerr.Wrapf(ErrStepTransitionInvalid, "%s -> %s", original, target)
}
step.State = next
}
return step, step.State != original, nil
}
func (s *svc) applyStepDiagnostics(step *agg.StepExecution, event *normalizedEvent) bool {
if step == nil || event == nil {
return false
}
now := s.now().UTC()
at := now
if eventAt := event.occurredAtValue(); eventAt != nil {
at = eventAt.UTC()
}
changed := false
switch step.State {
case agg.StepStateRunning:
if step.StartedAt == nil {
step.StartedAt = &at
changed = true
}
if step.CompletedAt != nil {
step.CompletedAt = nil
changed = true
}
if step.FailureCode != "" || step.FailureMsg != "" {
step.FailureCode = ""
step.FailureMsg = ""
changed = true
}
case agg.StepStateCompleted:
if step.StartedAt == nil {
step.StartedAt = &at
changed = true
}
if step.CompletedAt == nil || !step.CompletedAt.Equal(at) {
step.CompletedAt = &at
changed = true
}
if step.FailureCode != "" || step.FailureMsg != "" {
step.FailureCode = ""
step.FailureMsg = ""
changed = true
}
case agg.StepStateFailed, agg.StepStateNeedsAttention:
if step.StartedAt == nil {
step.StartedAt = &at
changed = true
}
if step.CompletedAt == nil || !step.CompletedAt.Equal(at) {
step.CompletedAt = &at
changed = true
}
fc := event.failureCodeValue()
fm := event.failureMsgValue()
if step.FailureCode != fc || step.FailureMsg != fm {
step.FailureCode = fc
step.FailureMsg = fm
changed = true
}
}
return changed
}
func clonePayment(payment *agg.Payment) (*agg.Payment, error) {
data, err := bson.Marshal(payment)
if err != nil {
return nil, merrors.Internal("payment clone failed")
}
var out agg.Payment
if err := bson.Unmarshal(data, &out); err != nil {
return nil, merrors.Internal("payment clone failed")
}
return &out, nil
}
func eventSource(event Event) string {
switch {
case event.Gateway != nil:
return "gateway"
case event.Ledger != nil:
return "ledger"
case event.Card != nil:
return "card"
default:
return "unknown"
}
}

View File

@@ -0,0 +1,366 @@
package erecon
import (
"errors"
"testing"
"time"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/pkg/merrors"
"go.uber.org/zap"
)
func TestReconcile_GatewayWaiting_UpdatesRunningAndRefs(t *testing.T) {
now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})
in := &agg.Payment{
PaymentRef: "p1",
State: agg.StateCreated,
Version: 7,
StepExecutions: []agg.StepExecution{
{StepRef: "s1", StepCode: "send", State: agg.StepStatePending, Attempt: 1},
},
}
out, err := reconciler.Reconcile(Input{
Payment: in,
Event: Event{
Gateway: &GatewayEvent{
StepRef: "s1",
OperationRef: "op-1",
TransferRef: "tx-1",
GatewayInstanceID: "gw-1",
Status: GatewayStatusWaiting,
},
},
})
if err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
if !out.StepChanged {
t.Fatal("expected step_changed")
}
if !out.AggregateChanged {
t.Fatal("expected aggregate_changed")
}
got := out.Payment.StepExecutions[0]
if got.State != agg.StepStateRunning {
t.Fatalf("step state mismatch: got=%q want=%q", got.State, agg.StepStateRunning)
}
if got.StartedAt == nil || !got.StartedAt.Equal(now) {
t.Fatalf("started_at mismatch: got=%v want=%v", got.StartedAt, now)
}
if got.CompletedAt != nil {
t.Fatalf("expected nil completed_at, got %v", got.CompletedAt)
}
if !hasRef(got.ExternalRefs, agg.ExternalRef{GatewayInstanceID: "gw-1", Kind: ExternalRefKindOperation, Ref: "op-1"}) {
t.Fatalf("expected operation_ref external reference")
}
if !hasRef(got.ExternalRefs, agg.ExternalRef{GatewayInstanceID: "gw-1", Kind: ExternalRefKindTransfer, Ref: "tx-1"}) {
t.Fatalf("expected transfer_ref external reference")
}
if out.Payment.State != agg.StateExecuting {
t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateExecuting)
}
if out.Payment.Version != 8 {
t.Fatalf("version mismatch: got=%d want=%d", out.Payment.Version, 8)
}
if in.StepExecutions[0].State != agg.StepStatePending {
t.Fatalf("input payment was mutated")
}
}
func TestReconcile_GatewaySuccess_SettlesPayment(t *testing.T) {
now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})
out, err := reconciler.Reconcile(Input{
Payment: &agg.Payment{
PaymentRef: "p1",
State: agg.StateCreated,
Version: 1,
StepExecutions: []agg.StepExecution{
{StepRef: "s1", StepCode: "observe", State: agg.StepStatePending, Attempt: 1},
},
},
Event: Event{
Gateway: &GatewayEvent{
StepRef: "s1",
Status: GatewayStatusSuccess,
OperationRef: "op-1",
},
},
})
if err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
step := out.Payment.StepExecutions[0]
if step.State != agg.StepStateCompleted {
t.Fatalf("step state mismatch: got=%q want=%q", step.State, agg.StepStateCompleted)
}
if step.CompletedAt == nil || !step.CompletedAt.Equal(now) {
t.Fatalf("completed_at mismatch: got=%v want=%v", step.CompletedAt, now)
}
if out.Payment.State != agg.StateSettled {
t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateSettled)
}
}
func TestReconcile_GatewayFailureMapping(t *testing.T) {
now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})
retryable := true
out, err := reconciler.Reconcile(Input{
Payment: &agg.Payment{
PaymentRef: "p1",
State: agg.StateExecuting,
StepExecutions: []agg.StepExecution{
{StepRef: "s1", StepCode: "observe", State: agg.StepStateRunning, Attempt: 1},
},
},
Event: Event{
Gateway: &GatewayEvent{
StepRef: "s1",
Status: GatewayStatusFailed,
Retryable: &retryable,
FailureCode: "gw_timeout",
FailureMsg: "timeout",
},
},
})
if err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
step := out.Payment.StepExecutions[0]
if step.State != agg.StepStateFailed {
t.Fatalf("step state mismatch: got=%q want=%q", step.State, agg.StepStateFailed)
}
if step.FailureCode != "gw_timeout" || step.FailureMsg != "timeout" {
t.Fatalf("failure details mismatch: code=%q msg=%q", step.FailureCode, step.FailureMsg)
}
if out.Payment.State != agg.StateExecuting {
t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateExecuting)
}
nonRetryable := false
out, err = reconciler.Reconcile(Input{
Payment: &agg.Payment{
PaymentRef: "p2",
State: agg.StateExecuting,
StepExecutions: []agg.StepExecution{
{StepRef: "s1", StepCode: "observe", State: agg.StepStateRunning, Attempt: 1},
},
},
Event: Event{
Gateway: &GatewayEvent{
StepRef: "s1",
Status: GatewayStatusFailed,
Retryable: &nonRetryable,
FailureCode: "gw_rejected",
FailureMsg: "rejected",
},
},
})
if err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
step = out.Payment.StepExecutions[0]
if step.State != agg.StepStateNeedsAttention {
t.Fatalf("step state mismatch: got=%q want=%q", step.State, agg.StepStateNeedsAttention)
}
if out.Payment.State != agg.StateNeedsAttention {
t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateNeedsAttention)
}
}
func TestReconcile_LedgerTerminalFailure_ForcesAggregateFailed(t *testing.T) {
now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})
out, err := reconciler.Reconcile(Input{
Payment: &agg.Payment{
PaymentRef: "p1",
State: agg.StateExecuting,
StepExecutions: []agg.StepExecution{
{
StepRef: "s1", StepCode: "ledger.debit", State: agg.StepStateRunning, Attempt: 1,
ExternalRefs: []agg.ExternalRef{{Kind: ExternalRefKindLedger, Ref: "entry-1"}},
},
},
},
Event: Event{
Ledger: &LedgerEvent{
EntryRef: "entry-1",
Status: LedgerStatusFailed,
FailureCode: "ledger_declined",
TerminalFailure: true,
},
},
})
if err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
if out.Payment.StepExecutions[0].State != agg.StepStateFailed {
t.Fatalf("step state mismatch: got=%q want=%q", out.Payment.StepExecutions[0].State, agg.StepStateFailed)
}
if out.Payment.State != agg.StateFailed {
t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateFailed)
}
}
func TestReconcile_CardMatchByExternalRef(t *testing.T) {
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: defaultNow})
out, err := reconciler.Reconcile(Input{
Payment: &agg.Payment{
PaymentRef: "p1",
State: agg.StateExecuting,
StepExecutions: []agg.StepExecution{
{
StepRef: "s1", StepCode: "card.observe", State: agg.StepStateRunning, Attempt: 1,
ExternalRefs: []agg.ExternalRef{
{Kind: ExternalRefKindCardPayout, Ref: "payout-1"},
},
},
},
},
Event: Event{
Card: &CardEvent{
PayoutRef: "payout-1",
Status: CardStatusSuccess,
},
},
})
if err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
if out.MatchedStepRef != "s1" {
t.Fatalf("matched step mismatch: got=%q want=%q", out.MatchedStepRef, "s1")
}
if out.Payment.StepExecutions[0].State != agg.StepStateCompleted {
t.Fatalf("step state mismatch: got=%q want=%q", out.Payment.StepExecutions[0].State, agg.StepStateCompleted)
}
}
func TestReconcile_MatchingErrors(t *testing.T) {
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: defaultNow})
_, err := reconciler.Reconcile(Input{
Payment: &agg.Payment{
PaymentRef: "p1",
State: agg.StateExecuting,
StepExecutions: []agg.StepExecution{
{
StepRef: "s1", StepCode: "a", State: agg.StepStateRunning,
ExternalRefs: []agg.ExternalRef{{Kind: ExternalRefKindTransfer, Ref: "tx-1"}},
},
{
StepRef: "s2", StepCode: "b", State: agg.StepStateRunning,
ExternalRefs: []agg.ExternalRef{{Kind: ExternalRefKindTransfer, Ref: "tx-1"}},
},
},
},
Event: Event{
Gateway: &GatewayEvent{
TransferRef: "tx-1",
Status: GatewayStatusSuccess,
},
},
})
if !errors.Is(err, ErrAmbiguousStepMatch) {
t.Fatalf("expected ErrAmbiguousStepMatch, got %v", err)
}
_, err = reconciler.Reconcile(Input{
Payment: &agg.Payment{
PaymentRef: "p1",
State: agg.StateExecuting,
StepExecutions: []agg.StepExecution{{StepRef: "s1", StepCode: "a", State: agg.StepStateRunning}},
},
Event: Event{
Gateway: &GatewayEvent{
TransferRef: "missing",
Status: GatewayStatusSuccess,
},
},
})
if !errors.Is(err, ErrStepNotFound) {
t.Fatalf("expected ErrStepNotFound, got %v", err)
}
}
func TestReconcile_ValidationErrors(t *testing.T) {
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: defaultNow})
tests := []struct {
name string
in Input
}{
{
name: "missing payment",
in: Input{
Event: Event{Gateway: &GatewayEvent{StepRef: "s1", Status: GatewayStatusSuccess}},
},
},
{
name: "missing step executions",
in: Input{
Payment: &agg.Payment{},
Event: Event{Gateway: &GatewayEvent{StepRef: "s1", Status: GatewayStatusSuccess}},
},
},
{
name: "multiple payloads",
in: Input{
Payment: &agg.Payment{
StepExecutions: []agg.StepExecution{{StepRef: "s1", StepCode: "a", State: agg.StepStatePending}},
},
Event: Event{
Gateway: &GatewayEvent{StepRef: "s1", Status: GatewayStatusSuccess},
Ledger: &LedgerEvent{StepRef: "s1", Status: LedgerStatusPosted},
},
},
},
{
name: "invalid status",
in: Input{
Payment: &agg.Payment{
StepExecutions: []agg.StepExecution{{StepRef: "s1", StepCode: "a", State: agg.StepStatePending}},
},
Event: Event{
Card: &CardEvent{StepRef: "s1", Status: CardStatus("bad")},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := reconciler.Reconcile(tt.in)
if !errors.Is(err, merrors.ErrInvalidArg) {
t.Fatalf("expected invalid argument error, got %v", err)
}
})
}
}
func hasRef(refs []agg.ExternalRef, wanted agg.ExternalRef) bool {
for i := range refs {
ref := refs[i]
if ref.Kind != wanted.Kind {
continue
}
if ref.Ref != wanted.Ref {
continue
}
if ref.GatewayInstanceID != wanted.GatewayInstanceID {
continue
}
return true
}
return false
}

View File

@@ -0,0 +1,28 @@
package idem
import (
"context"
"github.com/tech/sendico/payments/storage"
"github.com/tech/sendico/payments/storage/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
type fakeStore struct {
createFn func(ctx context.Context, payment *model.Payment) error
getByIdempotencyKeyFn func(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.Payment, error)
}
func (f *fakeStore) Create(ctx context.Context, payment *model.Payment) error {
if f.createFn == nil {
return nil
}
return f.createFn(ctx, payment)
}
func (f *fakeStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.Payment, error) {
if f.getByIdempotencyKeyFn == nil {
return nil, storage.ErrPaymentNotFound
}
return f.getByIdempotencyKeyFn(ctx, orgRef, idempotencyKey)
}

View File

@@ -4,13 +4,31 @@ import (
"crypto/sha256"
"encoding/hex"
"strings"
"time"
"github.com/tech/sendico/pkg/merrors"
"go.uber.org/zap"
)
const hashSep = "\x1f"
func (s *svc) Fingerprint(in FPInput) (string, error) {
func (s *svc) Fingerprint(in FPInput) (fingerprint string, err error) {
logger := s.logger
logger.Debug("Starting Fingerprint",
zap.String("organization_ref", strings.ToLower(strings.TrimSpace(in.OrganizationRef))),
zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
zap.String("intent_ref", strings.TrimSpace(in.IntentRef)),
zap.Bool("has_client_payment_ref", strings.TrimSpace(in.ClientPaymentRef) != ""),
)
defer func(start time.Time) {
fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
if err != nil {
logger.Warn("Failed to fingerprint", append(fields, zap.Error(err))...)
return
}
logger.Debug("Completed Fingerprint", append(fields, zap.Bool("generated", strings.TrimSpace(fingerprint) != ""))...)
}(time.Now())
orgRef := strings.ToLower(strings.TrimSpace(in.OrganizationRef))
if orgRef == "" {
return "", merrors.InvalidArgument("organization_ref is required")
@@ -29,7 +47,8 @@ func (s *svc) Fingerprint(in FPInput) (string, error) {
"client=" + clientPaymentRef,
}, hashSep)
return hashBytes([]byte(payload)), nil
fingerprint = hashBytes([]byte(payload))
return fingerprint, nil
}
func hashBytes(data []byte) string {

View File

@@ -0,0 +1,100 @@
package idem
import (
"testing"
)
func TestFingerprint_StableAndTrimmed(t *testing.T) {
svc := New()
a, err := svc.Fingerprint(FPInput{
OrganizationRef: " 65f1a2c6f3c5e2e7a1b2c3d4 ",
QuotationRef: " quote-1 ",
IntentRef: " intent-1 ",
ClientPaymentRef: " client-1 ",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
b, err := svc.Fingerprint(FPInput{
OrganizationRef: "65F1A2C6F3C5E2E7A1B2C3D4",
QuotationRef: "quote-1",
IntentRef: "intent-1",
ClientPaymentRef: "client-1",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
if a != b {
t.Fatalf("expected deterministic fingerprint, got %q vs %q", a, b)
}
}
func TestFingerprint_ChangesOnPayload(t *testing.T) {
svc := New()
base, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
QuotationRef: "quote-1",
IntentRef: "intent-1",
ClientPaymentRef: "client-1",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
diffQuote, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
QuotationRef: "quote-2",
IntentRef: "intent-1",
ClientPaymentRef: "client-1",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
if base == diffQuote {
t.Fatalf("expected different fingerprint for different quotation_ref")
}
diffClient, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
QuotationRef: "quote-1",
IntentRef: "intent-1",
ClientPaymentRef: "client-2",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
if base == diffClient {
t.Fatalf("expected different fingerprint for different client_payment_ref")
}
diffIntent, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
QuotationRef: "quote-1",
IntentRef: "intent-2",
ClientPaymentRef: "client-1",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
if base == diffIntent {
t.Fatalf("expected different fingerprint for different intent_ref")
}
}
func TestFingerprint_RequiresBusinessFields(t *testing.T) {
svc := New()
if _, err := svc.Fingerprint(FPInput{
QuotationRef: "quote-1",
}); err == nil {
t.Fatal("expected error for empty organization_ref")
}
if _, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
}); err == nil {
t.Fatal("expected error for empty quotation_ref")
}
}

View File

@@ -4,7 +4,9 @@ import (
"context"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
// Store is the minimal payment store contract required for idempotency handling.
@@ -41,6 +43,19 @@ type CreateInput struct {
Reuse ReuseInput
}
func New() Service {
return &svc{}
// Dependencies configures idempotency service integrations.
type Dependencies struct {
Logger mlogger.Logger
}
func New(deps ...Dependencies) Service {
var dep Dependencies
if len(deps) > 0 {
dep = deps[0]
}
logger := dep.Logger
if logger == nil {
logger = zap.NewNop()
}
return &svc{logger: logger.Named("idempotency")}
}

View File

@@ -10,101 +10,6 @@ import (
"go.mongodb.org/mongo-driver/v2/bson"
)
func TestFingerprint_StableAndTrimmed(t *testing.T) {
svc := New()
a, err := svc.Fingerprint(FPInput{
OrganizationRef: " 65f1a2c6f3c5e2e7a1b2c3d4 ",
QuotationRef: " quote-1 ",
IntentRef: " intent-1 ",
ClientPaymentRef: " client-1 ",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
b, err := svc.Fingerprint(FPInput{
OrganizationRef: "65F1A2C6F3C5E2E7A1B2C3D4",
QuotationRef: "quote-1",
IntentRef: "intent-1",
ClientPaymentRef: "client-1",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
if a != b {
t.Fatalf("expected deterministic fingerprint, got %q vs %q", a, b)
}
}
func TestFingerprint_ChangesOnPayload(t *testing.T) {
svc := New()
base, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
QuotationRef: "quote-1",
IntentRef: "intent-1",
ClientPaymentRef: "client-1",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
diffQuote, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
QuotationRef: "quote-2",
IntentRef: "intent-1",
ClientPaymentRef: "client-1",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
if base == diffQuote {
t.Fatalf("expected different fingerprint for different quotation_ref")
}
diffClient, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
QuotationRef: "quote-1",
IntentRef: "intent-1",
ClientPaymentRef: "client-2",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
if base == diffClient {
t.Fatalf("expected different fingerprint for different client_payment_ref")
}
diffIntent, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
QuotationRef: "quote-1",
IntentRef: "intent-2",
ClientPaymentRef: "client-1",
})
if err != nil {
t.Fatalf("Fingerprint returned error: %v", err)
}
if base == diffIntent {
t.Fatalf("expected different fingerprint for different intent_ref")
}
}
func TestFingerprint_RequiresBusinessFields(t *testing.T) {
svc := New()
if _, err := svc.Fingerprint(FPInput{
QuotationRef: "quote-1",
}); err == nil {
t.Fatal("expected error for empty organization_ref")
}
if _, err := svc.Fingerprint(FPInput{
OrganizationRef: "65f1a2c6f3c5e2e7a1b2c3d4",
}); err == nil {
t.Fatal("expected error for empty quotation_ref")
}
}
func TestTryReuse_NotFound(t *testing.T) {
svc := New()
store := &fakeStore{
@@ -294,22 +199,3 @@ func TestCreateOrReuse_DuplicateWithoutReusableRecordReturnsDuplicate(t *testing
t.Fatalf("expected ErrDuplicatePayment, got %v", err)
}
}
type fakeStore struct {
createFn func(ctx context.Context, payment *model.Payment) error
getByIdempotencyKeyFn func(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.Payment, error)
}
func (f *fakeStore) Create(ctx context.Context, payment *model.Payment) error {
if f.createFn == nil {
return nil
}
return f.createFn(ctx, payment)
}
func (f *fakeStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.Payment, error) {
if f.getByIdempotencyKeyFn == nil {
return nil, storage.ErrPaymentNotFound
}
return f.getByIdempotencyKeyFn(ctx, orgRef, idempotencyKey)
}

View File

@@ -4,21 +4,46 @@ import (
"context"
"errors"
"strings"
"time"
"github.com/tech/sendico/payments/storage"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
const reqHashMetaKey = "_orchestrator_v2_req_hash"
type svc struct{}
type svc struct {
logger mlogger.Logger
}
func (s *svc) TryReuse(
ctx context.Context,
store Store,
in ReuseInput,
) (*model.Payment, bool, error) {
) (payment *model.Payment, reused bool, err error) {
logger := s.logger
logger.Debug("Starting Try reuse",
zap.String("organization_ref", in.OrganizationID.Hex()),
zap.Bool("has_idempotency_key", strings.TrimSpace(in.IdempotencyKey) != ""),
)
defer func(start time.Time) {
fields := []zap.Field{
zap.Int64("duration_ms", time.Since(start).Milliseconds()),
zap.Bool("reused", reused),
}
if payment != nil {
fields = append(fields, zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)))
}
if err != nil {
logger.Warn("Failed to try reuse", append(fields, zap.Error(err))...)
return
}
logger.Debug("Completed Try reuse", fields...)
}(time.Now())
if store == nil {
return nil, false, merrors.InvalidArgument("payments store is required")
}
@@ -28,7 +53,7 @@ func (s *svc) TryReuse(
return nil, false, err
}
payment, err := store.GetByIdempotencyKey(ctx, in.OrganizationID, idempotencyKey)
payment, err = store.GetByIdempotencyKey(ctx, in.OrganizationID, idempotencyKey)
if err != nil {
if errors.Is(err, storage.ErrPaymentNotFound) || errors.Is(err, merrors.ErrNoData) {
return nil, false, nil
@@ -50,7 +75,28 @@ func (s *svc) CreateOrReuse(
ctx context.Context,
store Store,
in CreateInput,
) (*model.Payment, bool, error) {
) (payment *model.Payment, reused bool, err error) {
logger := s.logger
logger.Debug("Starting Create or reuse",
zap.String("organization_ref", in.Reuse.OrganizationID.Hex()),
zap.Bool("has_payment", in.Payment != nil),
zap.Bool("has_idempotency_key", strings.TrimSpace(in.Reuse.IdempotencyKey) != ""),
)
defer func(start time.Time) {
fields := []zap.Field{
zap.Int64("duration_ms", time.Since(start).Milliseconds()),
zap.Bool("reused", reused),
}
if payment != nil {
fields = append(fields, zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)))
}
if err != nil {
logger.Warn("Failed to create or reuse", append(fields, zap.Error(err))...)
return
}
logger.Debug("Completed Create or reuse", fields...)
}(time.Now())
if store == nil {
return nil, false, merrors.InvalidArgument("payments store is required")
}
@@ -64,19 +110,19 @@ func (s *svc) CreateOrReuse(
}
setPaymentReqHash(in.Payment, fingerprint)
if err := store.Create(ctx, in.Payment); err != nil {
if !errors.Is(err, storage.ErrDuplicatePayment) {
return nil, false, err
if createErr := store.Create(ctx, in.Payment); createErr != nil {
if !errors.Is(createErr, storage.ErrDuplicatePayment) {
return nil, false, createErr
}
payment, reused, reuseErr := s.TryReuse(ctx, store, in.Reuse)
if reuseErr != nil {
return nil, false, reuseErr
payment, reused, err = s.TryReuse(ctx, store, in.Reuse)
if err != nil {
return nil, false, err
}
if reused {
return payment, true, nil
}
return nil, false, err
return nil, false, createErr
}
return in.Payment, false, nil

View File

@@ -0,0 +1,111 @@
package oobs
import (
"strings"
"sync"
"github.com/tech/sendico/pkg/merrors"
)
type memoryAuditStore struct {
mu sync.RWMutex
byPayment map[string][]TimelineEntry
byStep map[string][]TimelineEntry
}
func newMemoryAuditStore() AuditStore {
return &memoryAuditStore{
byPayment: map[string][]TimelineEntry{},
byStep: map[string][]TimelineEntry{},
}
}
func (s *memoryAuditStore) Append(entry TimelineEntry) error {
paymentRef := strings.TrimSpace(entry.PaymentRef)
if paymentRef == "" {
return merrors.InvalidArgument("timeline.payment_ref is required")
}
stepRef := strings.TrimSpace(entry.StepRef)
entry.PaymentRef = paymentRef
entry.StepRef = stepRef
entry.StepCode = strings.TrimSpace(entry.StepCode)
entry.Event = strings.TrimSpace(entry.Event)
entry.State = strings.TrimSpace(entry.State)
entry.Message = strings.TrimSpace(entry.Message)
entry.Fields = trimStringMap(entry.Fields)
s.mu.Lock()
defer s.mu.Unlock()
s.byPayment[paymentRef] = append(s.byPayment[paymentRef], cloneTimelineEntry(entry))
if stepRef == "" {
return nil
}
key := stepAttemptKey(paymentRef, stepRef, entry.Attempt)
s.byStep[key] = append(s.byStep[key], cloneTimelineEntry(entry))
return nil
}
func (s *memoryAuditStore) ListByPayment(paymentRef string, limit int32, desc bool) ([]TimelineEntry, error) {
ref := strings.TrimSpace(paymentRef)
if ref == "" {
return nil, merrors.InvalidArgument("payment_ref is required")
}
limit = normalizeLimit(limit)
s.mu.RLock()
items := append([]TimelineEntry(nil), s.byPayment[ref]...)
s.mu.RUnlock()
return paginateEntries(items, limit, desc), nil
}
func (s *memoryAuditStore) ListByStepAttempt(
paymentRef string,
stepRef string,
attempt uint32,
limit int32,
desc bool,
) ([]TimelineEntry, error) {
ref := strings.TrimSpace(paymentRef)
if ref == "" {
return nil, merrors.InvalidArgument("payment_ref is required")
}
step := strings.TrimSpace(stepRef)
if step == "" {
return nil, merrors.InvalidArgument("step_ref is required")
}
if attempt == 0 {
return nil, merrors.InvalidArgument("attempt is required")
}
limit = normalizeLimit(limit)
key := stepAttemptKey(ref, step, attempt)
s.mu.RLock()
items := append([]TimelineEntry(nil), s.byStep[key]...)
s.mu.RUnlock()
return paginateEntries(items, limit, desc), nil
}
func paginateEntries(items []TimelineEntry, limit int32, desc bool) []TimelineEntry {
if desc {
items = reverseEntries(items)
}
if len(items) == 0 {
return nil
}
if int32(len(items)) > limit {
items = items[:limit]
}
out := make([]TimelineEntry, 0, len(items))
for i := range items {
out = append(out, cloneTimelineEntry(items[i]))
}
return out
}
func cloneTimelineEntry(entry TimelineEntry) TimelineEntry {
entry.Fields = cloneStringMap(entry.Fields)
return entry
}

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