separated quotation and payments

This commit is contained in:
Stephan D
2026-02-10 18:29:47 +01:00
parent 6745bc0f6f
commit 296cc7b86a
163 changed files with 13516 additions and 191 deletions

View File

@@ -0,0 +1,30 @@
module github.com/tech/sendico/payments/storage
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
require (
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
)
require (
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
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/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

171
api/payments/storage/go.sum Normal file
View File

@@ -0,0 +1,171 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
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/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/mongodb-adapter/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0lCi7JKrS4qQ=
github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
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/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=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,15 @@
package model
type OperationState string
const (
OperationStateCreated OperationState = "created" // record exists, not started
OperationStateProcessing OperationState = "processing" // we are working on it
OperationStatePlanned OperationState = "planned" // waiting for execution
OperationStateWaiting OperationState = "waiting" // waiting external world
OperationStateSuccess OperationState = "success" // final success
OperationStateFailed OperationState = "failed" // final failure
OperationStateCancelled OperationState = "cancelled" // final cancelled
OperationStateSkipped OperationState = "skipped" // final skipped
)

View File

@@ -0,0 +1,539 @@
package model
import (
"strings"
"time"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/model/account_role"
"github.com/tech/sendico/pkg/mservice"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
// PaymentKind captures the orchestrator intent type.
type PaymentKind string
const (
PaymentKindUnspecified PaymentKind = "unspecified"
PaymentKindPayout PaymentKind = "payout"
PaymentKindInternalTransfer PaymentKind = "internal_transfer"
PaymentKindFXConversion PaymentKind = "fx_conversion"
)
// SettlementMode defines how fees/FX variance is handled.
type SettlementMode string
const (
SettlementModeUnspecified SettlementMode = "unspecified"
SettlementModeFixSource SettlementMode = "fix_source"
SettlementModeFixReceived SettlementMode = "fix_received"
)
// CommitPolicy controls when a step is committed during orchestration.
type CommitPolicy string
const (
CommitPolicyUnspecified CommitPolicy = "UNSPECIFIED"
CommitPolicyImmediate CommitPolicy = "IMMEDIATE"
CommitPolicyAfterSuccess CommitPolicy = "AFTER_SUCCESS"
CommitPolicyAfterFailure CommitPolicy = "AFTER_FAILURE"
CommitPolicyAfterCanceled CommitPolicy = "AFTER_TERMINAL"
)
// PaymentState enumerates lifecycle phases.
type PaymentState string
const (
PaymentStateUnspecified PaymentState = "unspecified"
PaymentStateAccepted PaymentState = "accepted"
PaymentStateFundsReserved PaymentState = "funds_reserved"
PaymentStateSubmitted PaymentState = "submitted"
PaymentStateSuccess PaymentState = "success"
PaymentStateSettled PaymentState = "settled"
PaymentStateFailed PaymentState = "failed"
PaymentStateCancelled PaymentState = "cancelled"
)
// PaymentFailureCode captures terminal reasons.
type PaymentFailureCode string
const (
PaymentFailureCodeUnspecified PaymentFailureCode = "unspecified"
PaymentFailureCodeBalance PaymentFailureCode = "balance"
PaymentFailureCodeLedger PaymentFailureCode = "ledger"
PaymentFailureCodeFX PaymentFailureCode = "fx"
PaymentFailureCodeChain PaymentFailureCode = "chain"
PaymentFailureCodeFees PaymentFailureCode = "fees"
PaymentFailureCodePolicy PaymentFailureCode = "policy"
PaymentFailureCodeSettlement PaymentFailureCode = "settlement"
)
// Rail identifies a payment rail for orchestration.
type Rail string
const (
RailUnspecified Rail = "UNSPECIFIED"
RailCrypto Rail = "CRYPTO"
RailProviderSettlement Rail = "PROVIDER_SETTLEMENT"
RailLedger Rail = "LEDGER"
RailCardPayout Rail = "CARD_PAYOUT"
RailFiatOnRamp Rail = "FIAT_ONRAMP"
)
// RailOperation identifies an explicit action within a payment plan.
type RailOperation string
const (
RailOperationUnspecified RailOperation = "UNSPECIFIED"
RailOperationDebit RailOperation = "DEBIT"
RailOperationCredit RailOperation = "CREDIT"
RailOperationExternalDebit RailOperation = "EXTERNAL_DEBIT"
RailOperationExternalCredit RailOperation = "EXTERNAL_CREDIT"
RailOperationMove RailOperation = "MOVE"
RailOperationSend RailOperation = "SEND"
RailOperationFee RailOperation = "FEE"
RailOperationObserveConfirm RailOperation = "OBSERVE_CONFIRM"
RailOperationFXConvert RailOperation = "FX_CONVERT"
RailOperationBlock RailOperation = "BLOCK"
RailOperationRelease RailOperation = "RELEASE"
)
// RailCapabilities are declared per gateway instance.
type RailCapabilities struct {
CanPayIn bool `bson:"canPayIn,omitempty" json:"canPayIn,omitempty"`
CanPayOut bool `bson:"canPayOut,omitempty" json:"canPayOut,omitempty"`
CanReadBalance bool `bson:"canReadBalance,omitempty" json:"canReadBalance,omitempty"`
CanSendFee bool `bson:"canSendFee,omitempty" json:"canSendFee,omitempty"`
RequiresObserveConfirm bool `bson:"requiresObserveConfirm,omitempty" json:"requiresObserveConfirm,omitempty"`
CanBlock bool `bson:"canBlock,omitempty" json:"canBlock,omitempty"`
CanRelease bool `bson:"canRelease,omitempty" json:"canRelease,omitempty"`
}
// LimitsOverride applies per-currency overrides for limits.
type LimitsOverride struct {
MaxVolume string `bson:"maxVolume,omitempty" json:"maxVolume,omitempty"`
MinAmount string `bson:"minAmount,omitempty" json:"minAmount,omitempty"`
MaxAmount string `bson:"maxAmount,omitempty" json:"maxAmount,omitempty"`
MaxFee string `bson:"maxFee,omitempty" json:"maxFee,omitempty"`
MaxOps int `bson:"maxOps,omitempty" json:"maxOps,omitempty"`
}
// Limits define time-bucketed and per-tx constraints.
type Limits struct {
MinAmount string `bson:"minAmount,omitempty" json:"minAmount,omitempty"`
MaxAmount string `bson:"maxAmount,omitempty" json:"maxAmount,omitempty"`
PerTxMaxFee string `bson:"perTxMaxFee,omitempty" json:"perTxMaxFee,omitempty"`
PerTxMinAmount string `bson:"perTxMinAmount,omitempty" json:"perTxMinAmount,omitempty"`
PerTxMaxAmount string `bson:"perTxMaxAmount,omitempty" json:"perTxMaxAmount,omitempty"`
VolumeLimit map[string]string `bson:"volumeLimit,omitempty" json:"volumeLimit,omitempty"`
VelocityLimit map[string]int `bson:"velocityLimit,omitempty" json:"velocityLimit,omitempty"`
CurrencyLimits map[string]LimitsOverride `bson:"currencyLimits,omitempty" json:"currencyLimits,omitempty"`
}
// GatewayInstanceDescriptor standardizes gateway instance self-declaration.
type GatewayInstanceDescriptor struct {
ID string `bson:"id" json:"id"`
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
Rail Rail `bson:"rail" json:"rail"`
Network string `bson:"network,omitempty" json:"network,omitempty"`
InvokeURI string `bson:"invokeUri,omitempty" json:"invokeUri,omitempty"`
Currencies []string `bson:"currencies,omitempty" json:"currencies,omitempty"`
Capabilities RailCapabilities `bson:"capabilities,omitempty" json:"capabilities,omitempty"`
Limits Limits `bson:"limits,omitempty" json:"limits,omitempty"`
Version string `bson:"version,omitempty" json:"version,omitempty"`
IsEnabled bool `bson:"isEnabled" json:"isEnabled"`
}
// PaymentEndpointType indicates how value should be routed.
type PaymentEndpointType string
const (
EndpointTypeUnspecified PaymentEndpointType = "unspecified"
EndpointTypeLedger PaymentEndpointType = "ledger"
EndpointTypeManagedWallet PaymentEndpointType = "managed_wallet"
EndpointTypeExternalChain PaymentEndpointType = "external_chain"
EndpointTypeCard PaymentEndpointType = "card"
)
// LedgerEndpoint describes ledger routing.
type LedgerEndpoint struct {
LedgerAccountRef string `bson:"ledgerAccountRef" json:"ledgerAccountRef"`
ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty" json:"contraLedgerAccountRef,omitempty"`
}
// ManagedWalletEndpoint describes managed wallet routing.
type ManagedWalletEndpoint struct {
ManagedWalletRef string `bson:"managedWalletRef" json:"managedWalletRef"`
Asset *paymenttypes.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
}
// ExternalChainEndpoint describes an external address.
type ExternalChainEndpoint struct {
Asset *paymenttypes.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
Address string `bson:"address" json:"address"`
Memo string `bson:"memo,omitempty" json:"memo,omitempty"`
}
// CardEndpoint describes a card payout destination.
type CardEndpoint struct {
Pan string `bson:"pan,omitempty" json:"pan,omitempty"`
Token string `bson:"token,omitempty" json:"token,omitempty"`
Cardholder string `bson:"cardholder,omitempty" json:"cardholder,omitempty"`
CardholderSurname string `bson:"cardholderSurname,omitempty" json:"cardholderSurname,omitempty"`
ExpMonth uint32 `bson:"expMonth,omitempty" json:"expMonth,omitempty"`
ExpYear uint32 `bson:"expYear,omitempty" json:"expYear,omitempty"`
Country string `bson:"country,omitempty" json:"country,omitempty"`
MaskedPan string `bson:"maskedPan,omitempty" json:"maskedPan,omitempty"`
}
// CardPayout stores gateway payout tracking info.
type CardPayout struct {
PayoutRef string `bson:"payoutRef,omitempty" json:"payoutRef,omitempty"`
ProviderPaymentID string `bson:"providerPaymentId,omitempty" json:"providerPaymentId,omitempty"`
Status string `bson:"status,omitempty" json:"status,omitempty"`
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
CardCountry string `bson:"cardCountry,omitempty" json:"cardCountry,omitempty"`
MaskedPan string `bson:"maskedPan,omitempty" json:"maskedPan,omitempty"`
ProviderCode string `bson:"providerCode,omitempty" json:"providerCode,omitempty"`
GatewayReference string `bson:"gatewayReference,omitempty" json:"gatewayReference,omitempty"`
}
// PaymentEndpoint is a polymorphic payment destination/source.
type PaymentEndpoint struct {
Type PaymentEndpointType `bson:"type" json:"type"`
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
Ledger *LedgerEndpoint `bson:"ledger,omitempty" json:"ledger,omitempty"`
ManagedWallet *ManagedWalletEndpoint `bson:"managedWallet,omitempty" json:"managedWallet,omitempty"`
ExternalChain *ExternalChainEndpoint `bson:"externalChain,omitempty" json:"externalChain,omitempty"`
Card *CardEndpoint `bson:"card,omitempty" json:"card,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
}
// FXIntent captures FX conversion preferences.
type FXIntent struct {
Pair *paymenttypes.CurrencyPair `bson:"pair,omitempty" json:"pair,omitempty"`
Side paymenttypes.FXSide `bson:"side,omitempty" json:"side,omitempty"`
Firm bool `bson:"firm,omitempty" json:"firm,omitempty"`
TTLMillis int64 `bson:"ttlMillis,omitempty" json:"ttlMillis,omitempty"`
PreferredProvider string `bson:"preferredProvider,omitempty" json:"preferredProvider,omitempty"`
MaxAgeMillis int32 `bson:"maxAgeMillis,omitempty" json:"maxAgeMillis,omitempty"`
}
// PaymentIntent models the requested payment operation.
type PaymentIntent struct {
Ref string `bson:"ref" json:"ref"`
Kind PaymentKind `bson:"kind" json:"kind"`
Source PaymentEndpoint `bson:"source" json:"source"`
Destination PaymentEndpoint `bson:"destination" json:"destination"`
Amount *paymenttypes.Money `bson:"amount" json:"amount"`
RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"`
FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"`
FeePolicy *paymenttypes.FeePolicy `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"`
SettlementMode SettlementMode `bson:"settlementMode,omitempty" json:"settlementMode,omitempty"`
SettlementCurrency string `bson:"settlementCurrency,omitempty" json:"settlementCurrency,omitempty"`
Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,omitempty"`
Customer *Customer `bson:"customer,omitempty" json:"customer,omitempty"`
}
// Customer captures payer/recipient identity details for downstream processing.
type Customer struct {
ID string `bson:"id,omitempty" json:"id,omitempty"`
FirstName string `bson:"firstName,omitempty" json:"firstName,omitempty"`
MiddleName string `bson:"middleName,omitempty" json:"middleName,omitempty"`
LastName string `bson:"lastName,omitempty" json:"lastName,omitempty"`
IP string `bson:"ip,omitempty" json:"ip,omitempty"`
Zip string `bson:"zip,omitempty" json:"zip,omitempty"`
Country string `bson:"country,omitempty" json:"country,omitempty"`
State string `bson:"state,omitempty" json:"state,omitempty"`
City string `bson:"city,omitempty" json:"city,omitempty"`
Address string `bson:"address,omitempty" json:"address,omitempty"`
}
// PaymentQuoteSnapshot stores the latest quote info.
type PaymentQuoteSnapshot struct {
DebitAmount *paymenttypes.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"`
DebitSettlementAmount *paymenttypes.Money `bson:"debitSettlementAmount,omitempty" json:"debitSettlementAmount,omitempty"`
ExpectedSettlementAmount *paymenttypes.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"`
ExpectedFeeTotal *paymenttypes.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"`
FeeLines []*paymenttypes.FeeLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"`
FeeRules []*paymenttypes.AppliedRule `bson:"feeRules,omitempty" json:"feeRules,omitempty"`
FXQuote *paymenttypes.FXQuote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"`
NetworkFee *paymenttypes.NetworkFeeEstimate `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
QuoteRef string `bson:"quoteRef,omitempty" json:"quoteRef,omitempty"`
}
// ExecutionRefs links to downstream systems.
type ExecutionRefs struct {
DebitEntryRef string `bson:"debitEntryRef,omitempty" json:"debitEntryRef,omitempty"`
CreditEntryRef string `bson:"creditEntryRef,omitempty" json:"creditEntryRef,omitempty"`
FXEntryRef string `bson:"fxEntryRef,omitempty" json:"fxEntryRef,omitempty"`
ChainTransferRef string `bson:"chainTransferRef,omitempty" json:"chainTransferRef,omitempty"`
CardPayoutRef string `bson:"cardPayoutRef,omitempty" json:"cardPayoutRef,omitempty"`
FeeTransferRef string `bson:"feeTransferRef,omitempty" json:"feeTransferRef,omitempty"`
}
// PaymentStep is an explicit action within a payment plan.
type PaymentStep struct {
StepID string `bson:"stepId,omitempty" json:"stepId,omitempty"`
Rail Rail `bson:"rail" json:"rail"`
GatewayID string `bson:"gatewayId,omitempty" json:"gatewayId,omitempty"`
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
Action RailOperation `bson:"action" json:"action"`
DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"`
CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"`
CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"`
Amount *paymenttypes.Money `bson:"amount,omitempty" json:"amount,omitempty"`
FromRole *account_role.AccountRole `bson:"fromRole,omitempty" json:"fromRole,omitempty"`
ToRole *account_role.AccountRole `bson:"toRole,omitempty" json:"toRole,omitempty"`
}
// PaymentPlan captures the ordered list of steps to execute a payment.
type PaymentPlan struct {
ID string `bson:"id,omitempty" json:"id,omitempty"`
FXQuote *paymenttypes.FXQuote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"`
Fees []*paymenttypes.FeeLine `bson:"fees,omitempty" json:"fees,omitempty"`
Steps []*PaymentStep `bson:"steps,omitempty" json:"steps,omitempty"`
IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotencyKey,omitempty"`
CreatedAt time.Time `bson:"createdAt,omitempty" json:"createdAt,omitempty"`
}
// ExecutionStep describes a planned or executed payment step for reporting.
type ExecutionStep struct {
Code string `bson:"code,omitempty" json:"code,omitempty"`
Description string `bson:"description,omitempty" json:"description,omitempty"`
Amount *paymenttypes.Money `bson:"amount,omitempty" json:"amount,omitempty"`
NetworkFee *paymenttypes.Money `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
SourceWalletRef string `bson:"sourceWalletRef,omitempty" json:"sourceWalletRef,omitempty"`
DestinationRef string `bson:"destinationRef,omitempty" json:"destinationRef,omitempty"`
TransferRef string `bson:"transferRef,omitempty" json:"transferRef,omitempty"`
OperationRef string `bson:"operationRef,omitempty" json:"operationRef,omitempty"`
Error string `bson:"error,omitempty" json:"error,omitempty"`
State OperationState `bson:"state,omitempty" json:"state,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
}
func (s *ExecutionStep) IsTerminal() bool {
if s.State == OperationStateSuccess ||
s.State == OperationStateFailed ||
s.State == OperationStateCancelled ||
s.State == OperationStateSkipped {
return true
}
return false
}
func (s *ExecutionStep) IsSuccess() bool {
return s.State == OperationStateSuccess
}
func (s *ExecutionStep) IsFailed() bool {
return s.State == OperationStateFailed
}
func (s *ExecutionStep) ReadyForNext() bool {
switch s.State {
case OperationStateSuccess,
OperationStateSkipped:
return true
default:
return false
}
}
// ExecutionPlan captures the ordered list of steps to execute a payment.
type ExecutionPlan struct {
Steps []*ExecutionStep `bson:"steps,omitempty" json:"steps,omitempty"`
TotalNetworkFee *paymenttypes.Money `bson:"totalNetworkFee,omitempty" json:"totalNetworkFee,omitempty"`
}
// Payment persists orchestrated payment lifecycle.
type Payment struct {
storable.Base `bson:",inline" json:",inline"`
model.OrganizationBoundBase `bson:",inline" json:",inline"`
PaymentRef string `bson:"paymentRef" json:"paymentRef"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
Intent PaymentIntent `bson:"intent" json:"intent"`
State PaymentState `bson:"state" json:"state"`
FailureCode PaymentFailureCode `bson:"failureCode,omitempty" json:"failureCode,omitempty"`
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
LastQuote *PaymentQuoteSnapshot `bson:"lastQuote,omitempty" json:"lastQuote,omitempty"`
Execution *ExecutionRefs `bson:"execution,omitempty" json:"execution,omitempty"`
ExecutionPlan *ExecutionPlan `bson:"executionPlan,omitempty" json:"executionPlan,omitempty"`
PaymentPlan *PaymentPlan `bson:"paymentPlan,omitempty" json:"paymentPlan,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
CardPayout *CardPayout `bson:"cardPayout,omitempty" json:"cardPayout,omitempty"`
}
// Collection implements storable.Storable.
func (*Payment) Collection() string {
return mservice.Payments
}
// PaymentFilter enables filtered queries.
type PaymentFilter struct {
States []PaymentState
SourceRef string
OrganizationRef string
DestinationRef string
Cursor string
Limit int32
}
// PaymentList contains paginated results.
type PaymentList struct {
Items []*Payment
NextCursor string
}
// Normalize harmonises string fields for indexing and comparisons.
func (p *Payment) Normalize() {
p.PaymentRef = strings.TrimSpace(p.PaymentRef)
p.IdempotencyKey = strings.TrimSpace(p.IdempotencyKey)
p.FailureReason = strings.TrimSpace(p.FailureReason)
if p.Metadata != nil {
for k, v := range p.Metadata {
p.Metadata[k] = strings.TrimSpace(v)
}
}
normalizeEndpoint(&p.Intent.Source)
normalizeEndpoint(&p.Intent.Destination)
if p.Intent.Attributes != nil {
for k, v := range p.Intent.Attributes {
p.Intent.Attributes[k] = strings.TrimSpace(v)
}
}
p.Intent.SettlementCurrency = strings.TrimSpace(p.Intent.SettlementCurrency)
if p.Intent.Customer != nil {
p.Intent.Customer.ID = strings.TrimSpace(p.Intent.Customer.ID)
p.Intent.Customer.FirstName = strings.TrimSpace(p.Intent.Customer.FirstName)
p.Intent.Customer.MiddleName = strings.TrimSpace(p.Intent.Customer.MiddleName)
p.Intent.Customer.LastName = strings.TrimSpace(p.Intent.Customer.LastName)
p.Intent.Customer.IP = strings.TrimSpace(p.Intent.Customer.IP)
p.Intent.Customer.Zip = strings.TrimSpace(p.Intent.Customer.Zip)
p.Intent.Customer.Country = strings.TrimSpace(p.Intent.Customer.Country)
p.Intent.Customer.State = strings.TrimSpace(p.Intent.Customer.State)
p.Intent.Customer.City = strings.TrimSpace(p.Intent.Customer.City)
p.Intent.Customer.Address = strings.TrimSpace(p.Intent.Customer.Address)
}
if p.Execution != nil {
p.Execution.DebitEntryRef = strings.TrimSpace(p.Execution.DebitEntryRef)
p.Execution.CreditEntryRef = strings.TrimSpace(p.Execution.CreditEntryRef)
p.Execution.FXEntryRef = strings.TrimSpace(p.Execution.FXEntryRef)
p.Execution.ChainTransferRef = strings.TrimSpace(p.Execution.ChainTransferRef)
}
if p.ExecutionPlan != nil {
for _, step := range p.ExecutionPlan.Steps {
if step == nil {
continue
}
step.Code = strings.TrimSpace(step.Code)
step.Description = strings.TrimSpace(step.Description)
step.SourceWalletRef = strings.TrimSpace(step.SourceWalletRef)
step.DestinationRef = strings.TrimSpace(step.DestinationRef)
step.TransferRef = strings.TrimSpace(step.TransferRef)
if step.Metadata != nil {
for k, v := range step.Metadata {
step.Metadata[k] = strings.TrimSpace(v)
}
}
}
}
if p.PaymentPlan != nil {
p.PaymentPlan.ID = strings.TrimSpace(p.PaymentPlan.ID)
p.PaymentPlan.IdempotencyKey = strings.TrimSpace(p.PaymentPlan.IdempotencyKey)
for _, step := range p.PaymentPlan.Steps {
if step == nil {
continue
}
step.StepID = strings.TrimSpace(step.StepID)
step.Rail = Rail(strings.TrimSpace(string(step.Rail)))
step.GatewayID = strings.TrimSpace(step.GatewayID)
step.InstanceID = strings.TrimSpace(step.InstanceID)
step.Action = RailOperation(strings.TrimSpace(string(step.Action)))
step.CommitPolicy = normalizeCommitPolicy(step.CommitPolicy)
step.DependsOn = normalizeStringList(step.DependsOn)
step.CommitAfter = normalizeStringList(step.CommitAfter)
}
}
}
func normalizeEndpoint(ep *PaymentEndpoint) {
if ep == nil {
return
}
ep.InstanceID = strings.TrimSpace(ep.InstanceID)
if ep.Metadata != nil {
for k, v := range ep.Metadata {
ep.Metadata[k] = strings.TrimSpace(v)
}
}
switch ep.Type {
case EndpointTypeLedger:
if ep.Ledger != nil {
ep.Ledger.LedgerAccountRef = strings.TrimSpace(ep.Ledger.LedgerAccountRef)
ep.Ledger.ContraLedgerAccountRef = strings.TrimSpace(ep.Ledger.ContraLedgerAccountRef)
}
case EndpointTypeManagedWallet:
if ep.ManagedWallet != nil {
ep.ManagedWallet.ManagedWalletRef = strings.TrimSpace(ep.ManagedWallet.ManagedWalletRef)
if ep.ManagedWallet.Asset != nil {
ep.ManagedWallet.Asset.Chain = strings.TrimSpace(strings.ToUpper(ep.ManagedWallet.Asset.Chain))
ep.ManagedWallet.Asset.TokenSymbol = strings.TrimSpace(strings.ToUpper(ep.ManagedWallet.Asset.TokenSymbol))
ep.ManagedWallet.Asset.ContractAddress = strings.TrimSpace(strings.ToLower(ep.ManagedWallet.Asset.ContractAddress))
}
}
case EndpointTypeExternalChain:
if ep.ExternalChain != nil {
ep.ExternalChain.Address = strings.TrimSpace(strings.ToLower(ep.ExternalChain.Address))
ep.ExternalChain.Memo = strings.TrimSpace(ep.ExternalChain.Memo)
if ep.ExternalChain.Asset != nil {
ep.ExternalChain.Asset.Chain = strings.TrimSpace(strings.ToUpper(ep.ExternalChain.Asset.Chain))
ep.ExternalChain.Asset.TokenSymbol = strings.TrimSpace(strings.ToUpper(ep.ExternalChain.Asset.TokenSymbol))
ep.ExternalChain.Asset.ContractAddress = strings.TrimSpace(strings.ToLower(ep.ExternalChain.Asset.ContractAddress))
}
}
case EndpointTypeCard:
if ep.Card != nil {
ep.Card.Pan = strings.TrimSpace(ep.Card.Pan)
ep.Card.Token = strings.TrimSpace(ep.Card.Token)
ep.Card.Cardholder = strings.TrimSpace(ep.Card.Cardholder)
ep.Card.CardholderSurname = strings.TrimSpace(ep.Card.CardholderSurname)
ep.Card.Country = strings.TrimSpace(ep.Card.Country)
ep.Card.MaskedPan = strings.TrimSpace(ep.Card.MaskedPan)
}
}
}
func normalizeCommitPolicy(policy CommitPolicy) CommitPolicy {
val := strings.ToUpper(strings.TrimSpace(string(policy)))
switch CommitPolicy(val) {
case CommitPolicyImmediate, CommitPolicyAfterSuccess, CommitPolicyAfterFailure, CommitPolicyAfterCanceled:
return CommitPolicy(val)
default:
if val == "" {
return CommitPolicyUnspecified
}
return CommitPolicy(val)
}
}
func normalizeStringList(items []string) []string {
if len(items) == 0 {
return nil
}
result := make([]string, 0, len(items))
for _, item := range items {
clean := strings.TrimSpace(item)
if clean == "" {
continue
}
result = append(result, clean)
}
if len(result) == 0 {
return nil
}
return result
}

View File

@@ -0,0 +1,93 @@
package model
import (
"strings"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/model/account_role"
"github.com/tech/sendico/pkg/mservice"
)
// OrchestrationStep defines a template step for execution planning.
type OrchestrationStep struct {
StepID string `bson:"stepId" json:"stepId"`
Rail Rail `bson:"rail" json:"rail"`
Operation string `bson:"operation" json:"operation"`
DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"`
CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"`
CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"`
FromRole *account_role.AccountRole `bson:"fromRole,omitempty" json:"fromRole,omitempty"`
ToRole *account_role.AccountRole `bson:"toRole,omitempty" json:"toRole,omitempty"`
}
// PaymentPlanTemplate stores reusable orchestration templates.
type PaymentPlanTemplate struct {
storable.Base `bson:",inline" json:",inline"`
FromRail Rail `bson:"fromRail" json:"fromRail"`
ToRail Rail `bson:"toRail" json:"toRail"`
Network string `bson:"network,omitempty" json:"network,omitempty"`
Steps []OrchestrationStep `bson:"steps,omitempty" json:"steps,omitempty"`
IsEnabled bool `bson:"isEnabled" json:"isEnabled"`
}
// Collection implements storable.Storable.
func (*PaymentPlanTemplate) Collection() string {
return mservice.PaymentPlanTemplates
}
// Normalize standardizes template fields for matching and indexing.
func (t *PaymentPlanTemplate) Normalize() {
if t == nil {
return
}
t.FromRail = Rail(strings.ToUpper(strings.TrimSpace(string(t.FromRail))))
t.ToRail = Rail(strings.ToUpper(strings.TrimSpace(string(t.ToRail))))
t.Network = strings.ToUpper(strings.TrimSpace(t.Network))
if len(t.Steps) == 0 {
return
}
for i := range t.Steps {
step := &t.Steps[i]
step.StepID = strings.TrimSpace(step.StepID)
step.Rail = Rail(strings.ToUpper(strings.TrimSpace(string(step.Rail))))
step.Operation = strings.ToLower(strings.TrimSpace(step.Operation))
step.CommitPolicy = normalizeCommitPolicy(step.CommitPolicy)
step.DependsOn = normalizeStringList(step.DependsOn)
step.CommitAfter = normalizeStringList(step.CommitAfter)
step.FromRole = normalizeAccountRole(step.FromRole)
step.ToRole = normalizeAccountRole(step.ToRole)
}
}
func normalizeAccountRole(role *account_role.AccountRole) *account_role.AccountRole {
if role == nil {
return nil
}
trimmed := strings.TrimSpace(string(*role))
if trimmed == "" {
return nil
}
if parsed, ok := account_role.Parse(trimmed); ok {
if parsed == "" {
return nil
}
normalized := parsed
return &normalized
}
normalized := account_role.AccountRole(strings.ToLower(trimmed))
return &normalized
}
// PaymentPlanTemplateFilter selects templates for lookup.
type PaymentPlanTemplateFilter struct {
FromRail Rail
ToRail Rail
Network string
IsEnabled *bool
}
// PaymentPlanTemplateList holds template results.
type PaymentPlanTemplateList struct {
Items []*PaymentPlanTemplate
}

View File

@@ -0,0 +1,29 @@
package model
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/model"
)
// PaymentQuoteRecord stores a quoted payment snapshot for later execution.
type PaymentQuoteRecord struct {
storable.Base `bson:",inline" json:",inline"`
model.OrganizationBoundBase `bson:",inline" json:",inline"`
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"`
Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"`
Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"`
Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"`
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
PurgeAt time.Time `bson:"purgeAt,omitempty" json:"purgeAt,omitempty"`
Hash string `bson:"hash" json:"hash"`
}
// Collection implements storable.Storable.
func (*PaymentQuoteRecord) Collection() string {
return "payment_quotes"
}

View File

@@ -0,0 +1,47 @@
package model
import (
"strings"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/mservice"
)
// PaymentRoute defines an allowed rail transition for orchestration.
type PaymentRoute struct {
storable.Base `bson:",inline" json:",inline"`
FromRail Rail `bson:"fromRail" json:"fromRail"`
ToRail Rail `bson:"toRail" json:"toRail"`
Network string `bson:"network,omitempty" json:"network,omitempty"`
RequiresObserve bool `bson:"requiresObserve,omitempty" json:"requiresObserve,omitempty"`
IsEnabled bool `bson:"isEnabled" json:"isEnabled"`
}
// Collection implements storable.Storable.
func (*PaymentRoute) Collection() string {
return mservice.PaymentRoutes
}
// Normalize standardizes route fields for consistent indexing and matching.
func (r *PaymentRoute) Normalize() {
if r == nil {
return
}
r.FromRail = Rail(strings.ToUpper(strings.TrimSpace(string(r.FromRail))))
r.ToRail = Rail(strings.ToUpper(strings.TrimSpace(string(r.ToRail))))
r.Network = strings.ToUpper(strings.TrimSpace(r.Network))
}
// PaymentRouteFilter selects routes for lookup.
type PaymentRouteFilter struct {
FromRail Rail
ToRail Rail
Network string
IsEnabled *bool
}
// PaymentRouteList holds route results.
type PaymentRouteList struct {
Items []*PaymentRoute
}

View File

@@ -0,0 +1,136 @@
package mongo
import (
"context"
"time"
"github.com/tech/sendico/payments/storage"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/payments/storage/mongo/store"
quotemongo "github.com/tech/sendico/payments/storage/quote/mongo"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
)
// Store implements storage.Repository backed by MongoDB.
type Store struct {
logger mlogger.Logger
ping func(context.Context) error
payments storage.PaymentsStore
quotes storage.QuotesStore
routes storage.RoutesStore
plans storage.PlanTemplatesStore
}
type options struct {
quoteRetention time.Duration
}
// Option configures the Mongo-backed payments repository.
type Option func(*options)
// WithQuoteRetention sets how long payment quote records are retained after expiry.
func WithQuoteRetention(retention time.Duration) Option {
return func(opts *options) {
opts.quoteRetention = retention
}
}
// New constructs a Mongo-backed payments repository from a Mongo connection.
func New(logger mlogger.Logger, conn *db.MongoConnection, opts ...Option) (*Store, error) {
if conn == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: connection is nil")
}
paymentsRepo := repository.CreateMongoRepository(conn.Database(), (&model.Payment{}).Collection())
quotesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentQuoteRecord{}).Collection())
routesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentRoute{}).Collection())
plansRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentPlanTemplate{}).Collection())
return NewWithRepository(logger, conn.Ping, paymentsRepo, quotesRepo, routesRepo, plansRepo, opts...)
}
// NewWithRepository constructs a payments repository using the provided primitives.
func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, paymentsRepo repository.Repository, quotesRepo repository.Repository, routesRepo repository.Repository, plansRepo repository.Repository, opts ...Option) (*Store, error) {
if ping == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: ping func is nil")
}
if paymentsRepo == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: payments repository is nil")
}
if quotesRepo == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: quotes repository is nil")
}
if routesRepo == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: routes repository is nil")
}
if plansRepo == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: plan templates repository is nil")
}
cfg := options{}
for _, opt := range opts {
if opt != nil {
opt(&cfg)
}
}
childLogger := logger.Named("storage").Named("mongo")
paymentsStore, err := store.NewPayments(childLogger, paymentsRepo)
if err != nil {
return nil, err
}
quotesRepoStore, err := quotemongo.NewWithRepository(childLogger, ping, quotesRepo, quotemongo.WithQuoteRetention(cfg.quoteRetention))
if err != nil {
return nil, err
}
routesStore, err := store.NewRoutes(childLogger, routesRepo)
if err != nil {
return nil, err
}
plansStore, err := store.NewPlanTemplates(childLogger, plansRepo)
if err != nil {
return nil, err
}
result := &Store{
logger: childLogger,
ping: ping,
payments: paymentsStore,
quotes: quotesRepoStore.Quotes(),
routes: routesStore,
plans: plansStore,
}
return result, nil
}
// Ping verifies connectivity with the backing database.
func (s *Store) Ping(ctx context.Context) error {
if s.ping == nil {
return merrors.InvalidArgument("payments.storage.mongo: ping func is nil")
}
return s.ping(ctx)
}
// Payments returns the payments store.
func (s *Store) Payments() storage.PaymentsStore {
return s.payments
}
// Quotes returns the quotes store.
func (s *Store) Quotes() storage.QuotesStore {
return s.quotes
}
// Routes returns the routing store.
func (s *Store) Routes() storage.RoutesStore {
return s.routes
}
// PlanTemplates returns the plan templates store.
func (s *Store) PlanTemplates() storage.PlanTemplatesStore {
return s.plans
}
var _ storage.Repository = (*Store)(nil)

View File

@@ -0,0 +1,281 @@
package store
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/payments/storage"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
const (
defaultPaymentPageSize int64 = 50
maxPaymentPageSize int64 = 200
)
type Payments struct {
logger mlogger.Logger
repo repository.Repository
}
// NewPayments constructs a Mongo-backed payments store.
func NewPayments(logger mlogger.Logger, repo repository.Repository) (*Payments, error) {
if repo == nil {
return nil, merrors.InvalidArgument("paymentsStore: repository is nil")
}
indexes := []*ri.Definition{
{
Keys: []ri.Key{{Field: "paymentRef", Sort: ri.Asc}},
Unique: true,
},
{
Keys: []ri.Key{{Field: "idempotencyKey", Sort: ri.Asc}, {Field: "organizationRef", Sort: ri.Asc}},
Unique: true,
},
{
Keys: []ri.Key{{Field: "state", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "intent.source.managedWallet.managedWalletRef", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "intent.destination.managedWallet.managedWalletRef", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "execution.chainTransferRef", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "executionPlan.steps.transferRef", Sort: ri.Asc}},
},
}
for _, def := range indexes {
if err := repo.CreateIndex(def); err != nil {
logger.Error("failed to ensure payments index", zap.Error(err), zap.String("collection", repo.Collection()))
return nil, err
}
}
childLogger := logger.Named("payments")
childLogger.Debug("payments store initialised")
return &Payments{
logger: childLogger,
repo: repo,
}, nil
}
func (p *Payments) Create(ctx context.Context, payment *model.Payment) error {
if payment == nil {
return merrors.InvalidArgument("paymentsStore: nil payment")
}
payment.Normalize()
if payment.PaymentRef == "" {
return merrors.InvalidArgument("paymentsStore: empty paymentRef")
}
if strings.TrimSpace(payment.IdempotencyKey) == "" {
return merrors.InvalidArgument("paymentsStore: empty idempotencyKey")
}
if payment.OrganizationRef == bson.NilObjectID {
return merrors.InvalidArgument("paymentsStore: organization_ref is required")
}
payment.Update()
filter := repository.OrgFilter(payment.OrganizationRef).And(
repository.Filter("idempotencyKey", payment.IdempotencyKey),
)
if err := p.repo.Insert(ctx, payment, filter); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicatePayment
}
return err
}
p.logger.Debug("payment created", zap.String("payment_ref", payment.PaymentRef))
return nil
}
func (p *Payments) Update(ctx context.Context, payment *model.Payment) error {
if payment == nil {
return merrors.InvalidArgument("paymentsStore: nil payment")
}
if payment.ID.IsZero() {
return merrors.InvalidArgument("paymentsStore: missing payment id")
}
payment.Normalize()
payment.Update()
if err := p.repo.Update(ctx, payment); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return storage.ErrPaymentNotFound
}
return err
}
return nil
}
func (p *Payments) GetByPaymentRef(ctx context.Context, paymentRef string) (*model.Payment, error) {
paymentRef = strings.TrimSpace(paymentRef)
if paymentRef == "" {
return nil, merrors.InvalidArgument("paymentsStore: empty paymentRef")
}
entity := &model.Payment{}
if err := p.repo.FindOneByFilter(ctx, repository.Filter("paymentRef", paymentRef), entity); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrPaymentNotFound
}
return nil, err
}
return entity, nil
}
func (p *Payments) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.Payment, error) {
idempotencyKey = strings.TrimSpace(idempotencyKey)
if orgRef == bson.NilObjectID {
return nil, merrors.InvalidArgument("paymentsStore: organization_ref is required")
}
if idempotencyKey == "" {
return nil, merrors.InvalidArgument("paymentsStore: empty idempotencyKey")
}
entity := &model.Payment{}
query := repository.OrgFilter(orgRef).And(repository.Filter("idempotencyKey", idempotencyKey))
if err := p.repo.FindOneByFilter(ctx, query, entity); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrPaymentNotFound
}
return nil, err
}
return entity, nil
}
func (p *Payments) GetByChainTransferRef(ctx context.Context, transferRef string) (*model.Payment, error) {
transferRef = strings.TrimSpace(transferRef)
if transferRef == "" {
return nil, merrors.InvalidArgument("paymentsStore: empty chain transfer reference")
}
entity := &model.Payment{}
query := repository.Query().Or(
repository.Filter("execution.chainTransferRef", transferRef),
repository.Filter("executionPlan.steps.transferRef", transferRef),
)
if err := p.repo.FindOneByFilter(ctx, query, entity); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrPaymentNotFound
}
return nil, err
}
return entity, nil
}
func (p *Payments) List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error) {
if filter == nil {
filter = &model.PaymentFilter{}
}
query := repository.Query()
if len(filter.States) > 0 {
states := make([]string, 0, len(filter.States))
for _, state := range filter.States {
if trimmed := strings.TrimSpace(string(state)); trimmed != "" {
states = append(states, trimmed)
}
}
if len(states) > 0 {
query = query.Comparison(repository.Field("state"), builder.In, states)
}
}
if ref := strings.TrimSpace(filter.SourceRef); ref != "" {
if endpointFilter := endpointQuery("intent.source", ref); endpointFilter != nil {
query = query.And(endpointFilter)
}
}
if orgRef, err := bson.ObjectIDFromHex(strings.TrimSpace(filter.OrganizationRef)); err != nil {
p.logger.Warn("Failed to decode organization reference", zap.Error(err),
zap.String("provided_org_ref", filter.OrganizationRef))
return nil, err
} else {
query.And(repository.OrgFilter(orgRef))
}
if ref := strings.TrimSpace(filter.DestinationRef); ref != "" {
if endpointFilter := endpointQuery("intent.destination", ref); endpointFilter != nil {
query = query.And(endpointFilter)
}
}
if cursor := strings.TrimSpace(filter.Cursor); cursor != "" {
if oid, err := bson.ObjectIDFromHex(cursor); err == nil {
query = query.Comparison(repository.IDField(), builder.Gt, oid)
} else {
p.logger.Warn("ignoring invalid payments cursor", zap.String("cursor", cursor), zap.Error(err))
}
}
limit := sanitizePaymentLimit(filter.Limit)
fetchLimit := limit + 1
query = query.Sort(repository.IDField(), true).Limit(&fetchLimit)
payments := make([]*model.Payment, 0, fetchLimit)
decoder := func(cur *mongo.Cursor) error {
item := &model.Payment{}
if err := cur.Decode(item); err != nil {
p.logger.Warn("Failed to decode item", zap.Error(err))
return err
}
payments = append(payments, item)
return nil
}
if err := p.repo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) {
return nil, err
}
nextCursor := ""
if int64(len(payments)) == fetchLimit {
last := payments[len(payments)-1]
nextCursor = last.ID.Hex()
payments = payments[:len(payments)-1]
}
return &model.PaymentList{
Items: payments,
NextCursor: nextCursor,
}, nil
}
func endpointQuery(prefix, ref string) builder.Query {
trimmed := strings.TrimSpace(ref)
if trimmed == "" {
return nil
}
lower := strings.ToLower(trimmed)
filters := []builder.Query{
repository.Filter(prefix+".ledger.ledgerAccountRef", trimmed),
repository.Filter(prefix+".managedWallet.managedWalletRef", trimmed),
repository.Filter(prefix+".externalChain.address", lower),
}
return repository.Query().Or(filters...)
}
func sanitizePaymentLimit(requested int32) int64 {
if requested <= 0 {
return defaultPaymentPageSize
}
if requested > int32(maxPaymentPageSize) {
return maxPaymentPageSize
}
return int64(requested)
}

View File

@@ -0,0 +1,168 @@
package store
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/payments/storage"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
type PlanTemplates struct {
logger mlogger.Logger
repo repository.Repository
}
// NewPlanTemplates constructs a Mongo-backed plan template store.
func NewPlanTemplates(logger mlogger.Logger, repo repository.Repository) (*PlanTemplates, error) {
if repo == nil {
return nil, merrors.InvalidArgument("planTemplatesStore: repository is nil")
}
indexes := []*ri.Definition{
{
Keys: []ri.Key{
{Field: "fromRail", Sort: ri.Asc},
{Field: "toRail", Sort: ri.Asc},
{Field: "network", Sort: ri.Asc},
},
Unique: true,
},
{
Keys: []ri.Key{{Field: "fromRail", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "toRail", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "isEnabled", Sort: ri.Asc}},
},
}
for _, def := range indexes {
if err := repo.CreateIndex(def); err != nil {
logger.Error("failed to ensure plan templates index", zap.Error(err), zap.String("collection", repo.Collection()))
return nil, err
}
}
return &PlanTemplates{
logger: logger.Named("plan_templates"),
repo: repo,
}, nil
}
func (p *PlanTemplates) Create(ctx context.Context, template *model.PaymentPlanTemplate) error {
if template == nil {
return merrors.InvalidArgument("planTemplatesStore: nil template")
}
template.Normalize()
if template.FromRail == "" || template.FromRail == model.RailUnspecified {
return merrors.InvalidArgument("planTemplatesStore: from_rail is required")
}
if template.ToRail == "" || template.ToRail == model.RailUnspecified {
return merrors.InvalidArgument("planTemplatesStore: to_rail is required")
}
if len(template.Steps) == 0 {
return merrors.InvalidArgument("planTemplatesStore: steps are required")
}
if template.ID.IsZero() {
template.SetID(bson.NewObjectID())
} else {
template.Update()
}
filter := repository.Filter("fromRail", template.FromRail).And(
repository.Filter("toRail", template.ToRail),
repository.Filter("network", template.Network),
)
if err := p.repo.Insert(ctx, template, filter); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicatePlanTemplate
}
return err
}
return nil
}
func (p *PlanTemplates) Update(ctx context.Context, template *model.PaymentPlanTemplate) error {
if template == nil {
return merrors.InvalidArgument("planTemplatesStore: nil template")
}
if template.ID.IsZero() {
return merrors.InvalidArgument("planTemplatesStore: missing template id")
}
template.Normalize()
template.Update()
if err := p.repo.Update(ctx, template); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return storage.ErrPlanTemplateNotFound
}
return err
}
return nil
}
func (p *PlanTemplates) GetByID(ctx context.Context, id bson.ObjectID) (*model.PaymentPlanTemplate, error) {
if id == bson.NilObjectID {
return nil, merrors.InvalidArgument("planTemplatesStore: template id is required")
}
entity := &model.PaymentPlanTemplate{}
if err := p.repo.Get(ctx, id, entity); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrPlanTemplateNotFound
}
return nil, err
}
return entity, nil
}
func (p *PlanTemplates) List(ctx context.Context, filter *model.PaymentPlanTemplateFilter) (*model.PaymentPlanTemplateList, error) {
if filter == nil {
filter = &model.PaymentPlanTemplateFilter{}
}
query := repository.Query()
if from := strings.ToUpper(strings.TrimSpace(string(filter.FromRail))); from != "" {
query = query.Filter(repository.Field("fromRail"), from)
}
if to := strings.ToUpper(strings.TrimSpace(string(filter.ToRail))); to != "" {
query = query.Filter(repository.Field("toRail"), to)
}
if network := strings.ToUpper(strings.TrimSpace(filter.Network)); network != "" {
query = query.Filter(repository.Field("network"), network)
}
if filter.IsEnabled != nil {
query = query.Filter(repository.Field("isEnabled"), *filter.IsEnabled)
}
templates := make([]*model.PaymentPlanTemplate, 0)
decoder := func(cur *mongo.Cursor) error {
item := &model.PaymentPlanTemplate{}
if err := cur.Decode(item); err != nil {
return err
}
templates = append(templates, item)
return nil
}
if err := p.repo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) {
return nil, err
}
return &model.PaymentPlanTemplateList{
Items: templates,
}, nil
}
var _ storage.PlanTemplatesStore = (*PlanTemplates)(nil)

View File

@@ -0,0 +1,165 @@
package store
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/payments/storage"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
type Routes struct {
logger mlogger.Logger
repo repository.Repository
}
// NewRoutes constructs a Mongo-backed routes store.
func NewRoutes(logger mlogger.Logger, repo repository.Repository) (*Routes, error) {
if repo == nil {
return nil, merrors.InvalidArgument("routesStore: repository is nil")
}
indexes := []*ri.Definition{
{
Keys: []ri.Key{
{Field: "fromRail", Sort: ri.Asc},
{Field: "toRail", Sort: ri.Asc},
{Field: "network", Sort: ri.Asc},
},
Unique: true,
},
{
Keys: []ri.Key{{Field: "fromRail", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "toRail", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "isEnabled", Sort: ri.Asc}},
},
}
for _, def := range indexes {
if err := repo.CreateIndex(def); err != nil {
logger.Error("failed to ensure routes index", zap.Error(err), zap.String("collection", repo.Collection()))
return nil, err
}
}
return &Routes{
logger: logger.Named("routes"),
repo: repo,
}, nil
}
func (r *Routes) Create(ctx context.Context, route *model.PaymentRoute) error {
if route == nil {
return merrors.InvalidArgument("routesStore: nil route")
}
route.Normalize()
if route.FromRail == "" || route.FromRail == model.RailUnspecified {
return merrors.InvalidArgument("routesStore: from_rail is required")
}
if route.ToRail == "" || route.ToRail == model.RailUnspecified {
return merrors.InvalidArgument("routesStore: to_rail is required")
}
if route.ID.IsZero() {
route.SetID(bson.NewObjectID())
} else {
route.Update()
}
filter := repository.Filter("fromRail", route.FromRail).And(
repository.Filter("toRail", route.ToRail),
repository.Filter("network", route.Network),
)
if err := r.repo.Insert(ctx, route, filter); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicateRoute
}
return err
}
return nil
}
func (r *Routes) Update(ctx context.Context, route *model.PaymentRoute) error {
if route == nil {
return merrors.InvalidArgument("routesStore: nil route")
}
if route.ID.IsZero() {
return merrors.InvalidArgument("routesStore: missing route id")
}
route.Normalize()
route.Update()
if err := r.repo.Update(ctx, route); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return storage.ErrRouteNotFound
}
return err
}
return nil
}
func (r *Routes) GetByID(ctx context.Context, id bson.ObjectID) (*model.PaymentRoute, error) {
if id == bson.NilObjectID {
return nil, merrors.InvalidArgument("routesStore: route id is required")
}
entity := &model.PaymentRoute{}
if err := r.repo.Get(ctx, id, entity); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrRouteNotFound
}
return nil, err
}
return entity, nil
}
func (r *Routes) List(ctx context.Context, filter *model.PaymentRouteFilter) (*model.PaymentRouteList, error) {
if filter == nil {
filter = &model.PaymentRouteFilter{}
}
query := repository.Query()
if from := strings.ToUpper(strings.TrimSpace(string(filter.FromRail))); from != "" {
query = query.Filter(repository.Field("fromRail"), from)
}
if to := strings.ToUpper(strings.TrimSpace(string(filter.ToRail))); to != "" {
query = query.Filter(repository.Field("toRail"), to)
}
if network := strings.ToUpper(strings.TrimSpace(filter.Network)); network != "" {
query = query.Filter(repository.Field("network"), network)
}
if filter.IsEnabled != nil {
query = query.Filter(repository.Field("isEnabled"), *filter.IsEnabled)
}
routes := make([]*model.PaymentRoute, 0)
decoder := func(cur *mongo.Cursor) error {
item := &model.PaymentRoute{}
if err := cur.Decode(item); err != nil {
return err
}
routes = append(routes, item)
return nil
}
if err := r.repo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) {
return nil, err
}
return &model.PaymentRouteList{
Items: routes,
}, nil
}
var _ storage.RoutesStore = (*Routes)(nil)

View File

@@ -0,0 +1,90 @@
package mongo
import (
"context"
"time"
"github.com/tech/sendico/payments/storage/model"
quotestorage "github.com/tech/sendico/payments/storage/quote"
"github.com/tech/sendico/payments/storage/quote/mongo/store"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
)
// Store implements quotestorage.Repository backed by MongoDB.
type Store struct {
logger mlogger.Logger
ping func(context.Context) error
quotes quotestorage.QuotesStore
}
type options struct {
quoteRetention time.Duration
}
// Option configures the Mongo-backed quotes repository.
type Option func(*options)
// WithQuoteRetention sets how long quote records are retained after expiry.
func WithQuoteRetention(retention time.Duration) Option {
return func(opts *options) {
opts.quoteRetention = retention
}
}
// New constructs a Mongo-backed quotes repository from a Mongo connection.
func New(logger mlogger.Logger, conn *db.MongoConnection, opts ...Option) (*Store, error) {
if conn == nil {
return nil, merrors.InvalidArgument("payments.quote.storage.mongo: connection is nil")
}
quotesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentQuoteRecord{}).Collection())
return NewWithRepository(logger, conn.Ping, quotesRepo, opts...)
}
// NewWithRepository constructs a quotes repository using the provided primitives.
func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, quotesRepo repository.Repository, opts ...Option) (*Store, error) {
if ping == nil {
return nil, merrors.InvalidArgument("payments.quote.storage.mongo: ping func is nil")
}
if quotesRepo == nil {
return nil, merrors.InvalidArgument("payments.quote.storage.mongo: quotes repository is nil")
}
cfg := options{}
for _, opt := range opts {
if opt != nil {
opt(&cfg)
}
}
childLogger := logger.Named("quote_storage").Named("mongo")
quotesStore, err := store.NewQuotes(childLogger, quotesRepo, cfg.quoteRetention)
if err != nil {
return nil, err
}
result := &Store{
logger: childLogger,
ping: ping,
quotes: quotesStore,
}
return result, nil
}
// Ping verifies connectivity with the backing database.
func (s *Store) Ping(ctx context.Context) error {
if s.ping == nil {
return merrors.InvalidArgument("payments.quote.storage.mongo: ping func is nil")
}
return s.ping(ctx)
}
// Quotes returns the quotes store.
func (s *Store) Quotes() quotestorage.QuotesStore {
return s.quotes
}
var _ quotestorage.Repository = (*Store)(nil)

View File

@@ -0,0 +1,184 @@
package store
import (
"context"
"errors"
"strings"
"time"
"github.com/tech/sendico/payments/storage/model"
quotestorage "github.com/tech/sendico/payments/storage/quote"
"github.com/tech/sendico/pkg/db/repository"
"github.com/tech/sendico/pkg/db/repository/builder"
ri "github.com/tech/sendico/pkg/db/repository/index"
"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"
)
type Quotes struct {
logger mlogger.Logger
repo repository.Repository
retention time.Duration
}
const defaultPaymentQuoteRetention = 72 * time.Hour
// NewQuotes constructs a Mongo-backed quotes store.
func NewQuotes(logger mlogger.Logger, repo repository.Repository, retention time.Duration) (*Quotes, error) {
if repo == nil {
return nil, merrors.InvalidArgument("quotesStore: repository is nil")
}
if retention <= 0 {
logger.Info("Using default retention duration", zap.Duration("default_retention", defaultPaymentQuoteRetention))
retention = defaultPaymentQuoteRetention
}
logger.Info("Using retention duration", zap.Duration("retention", retention))
indexes := []*ri.Definition{
{
Keys: []ri.Key{{Field: "quoteRef", Sort: ri.Asc}},
Unique: true,
},
{
Keys: []ri.Key{
{Field: "organizationRef", Sort: ri.Asc},
{Field: "idempotencyKey", Sort: ri.Asc},
},
Unique: true,
Name: "payment_quotes_org_idempotency_key",
PartialFilter: repository.Query().Comparison(repository.Field("idempotencyKey"), builder.Exists, true),
},
{
Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "purgeAt", Sort: ri.Asc}},
TTL: int32Ptr(0),
Name: "payment_quotes_purge_at_ttl",
},
}
for _, def := range indexes {
if err := repo.CreateIndex(def); err != nil {
logger.Error("failed to ensure quotes index", zap.Error(err), zap.String("collection", repo.Collection()))
return nil, err
}
}
return &Quotes{
logger: logger.Named("quotes"),
repo: repo,
retention: retention,
}, nil
}
func (q *Quotes) Create(ctx context.Context, quote *model.PaymentQuoteRecord) error {
if quote == nil {
return merrors.InvalidArgument("quotesStore: nil quote")
}
quote.QuoteRef = strings.TrimSpace(quote.QuoteRef)
if quote.QuoteRef == "" {
return merrors.InvalidArgument("quotesStore: empty quoteRef")
}
if quote.OrganizationRef == bson.NilObjectID {
return merrors.InvalidArgument("quotesStore: organization_ref is required")
}
quote.IdempotencyKey = strings.TrimSpace(quote.IdempotencyKey)
if quote.IdempotencyKey == "" {
return merrors.InvalidArgument("quotesStore: idempotency key is required")
}
if quote.ExpiresAt.IsZero() {
return merrors.InvalidArgument("quotesStore: expires_at is required")
}
if quote.PurgeAt.IsZero() || quote.PurgeAt.Before(quote.ExpiresAt) {
quote.PurgeAt = quote.ExpiresAt.Add(q.retention)
}
if quote.Intent.Attributes != nil {
for k, v := range quote.Intent.Attributes {
quote.Intent.Attributes[k] = strings.TrimSpace(v)
}
}
if len(quote.Intents) > 0 {
for i := range quote.Intents {
if quote.Intents[i].Attributes == nil {
continue
}
for k, v := range quote.Intents[i].Attributes {
quote.Intents[i].Attributes[k] = strings.TrimSpace(v)
}
}
}
quote.Update()
filter := repository.OrgFilter(quote.OrganizationRef).And(
repository.Filter("quoteRef", quote.QuoteRef),
)
if err := q.repo.Insert(ctx, quote, filter); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
return quotestorage.ErrDuplicateQuote
}
q.logger.Warn("Failed to insert quote", mzap.ObjRef("org_ref", quote.OrganizationRef), zap.String("quote_ref", quote.QuoteRef), zap.Error(err))
return err
}
return nil
}
func (q *Quotes) GetByRef(ctx context.Context, orgRef bson.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error) {
quoteRef = strings.TrimSpace(quoteRef)
if quoteRef == "" {
return nil, merrors.InvalidArgument("quotesStore: empty quoteRef")
}
if orgRef == bson.NilObjectID {
return nil, merrors.InvalidArgument("quotesStore: organization_ref is required")
}
entity := &model.PaymentQuoteRecord{}
query := repository.OrgFilter(orgRef).And(repository.Filter("quoteRef", quoteRef))
if err := q.repo.FindOneByFilter(ctx, query, entity); err != nil {
if errors.Is(err, merrors.ErrNoData) {
q.logger.Debug("Quote not found by ref", zap.String("quote_ref", quoteRef), mzap.ObjRef("org_ref", orgRef))
return nil, quotestorage.ErrQuoteNotFound
}
q.logger.Warn("Failed to fetch quote by ref", zap.String("quote_ref", quoteRef), mzap.ObjRef("org_ref", orgRef), zap.Error(err))
return nil, err
}
if !entity.ExpiresAt.IsZero() && time.Now().After(entity.ExpiresAt) {
q.logger.Debug("Quote expired by idempotency key", zap.String("quote_ref", quoteRef), mzap.ObjRef("org_ref", orgRef), zap.Time("expires_at", entity.ExpiresAt))
return nil, quotestorage.ErrQuoteNotFound
}
return entity, nil
}
func (q *Quotes) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.PaymentQuoteRecord, error) {
idempotencyKey = strings.TrimSpace(idempotencyKey)
if idempotencyKey == "" {
return nil, merrors.InvalidArgument("quotesStore: empty idempotency key")
}
if orgRef == bson.NilObjectID {
return nil, merrors.InvalidArgument("quotesStore: organization_ref is required")
}
entity := &model.PaymentQuoteRecord{}
query := repository.OrgFilter(orgRef).And(repository.Filter("idempotencyKey", idempotencyKey))
if err := q.repo.FindOneByFilter(ctx, query, entity); err != nil {
if errors.Is(err, merrors.ErrNoData) {
q.logger.Debug("Quote not found by idempotency key", zap.String("idempotency_key", idempotencyKey), mzap.ObjRef("org_ref", orgRef))
return nil, quotestorage.ErrQuoteNotFound
}
q.logger.Warn("Failed to fetch quoteby idempotency key", zap.String("idempotency_key", idempotencyKey), mzap.ObjRef("org_ref", orgRef))
return nil, err
}
if !entity.ExpiresAt.IsZero() && time.Now().After(entity.ExpiresAt) {
q.logger.Debug("Quote expired by idempotency key", zap.String("idempotency_key", idempotencyKey), mzap.ObjRef("org_ref", orgRef), zap.Time("expires_at", entity.ExpiresAt))
return nil, quotestorage.ErrQuoteNotFound
}
return entity, nil
}
var _ quotestorage.QuotesStore = (*Quotes)(nil)
func int32Ptr(v int32) *int32 {
return &v
}

View File

@@ -0,0 +1,34 @@
package storage
import (
"context"
"github.com/tech/sendico/payments/storage/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
type storageError string
func (e storageError) Error() string {
return string(e)
}
var (
// ErrQuoteNotFound signals that a stored quote does not exist or expired.
ErrQuoteNotFound = storageError("payments.storage.quote: quote not found")
// ErrDuplicateQuote signals that a quote reference already exists.
ErrDuplicateQuote = storageError("payments.storage.quote: duplicate quote")
)
// Repository exposes persistence primitives for quote records.
type Repository interface {
Ping(ctx context.Context) error
Quotes() QuotesStore
}
// QuotesStore manages temporary stored payment quotes.
type QuotesStore interface {
Create(ctx context.Context, quote *model.PaymentQuoteRecord) error
GetByRef(ctx context.Context, orgRef bson.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error)
GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.PaymentQuoteRecord, error)
}

View File

@@ -0,0 +1,75 @@
package storage
import (
"context"
"github.com/tech/sendico/payments/storage/model"
quotestorage "github.com/tech/sendico/payments/storage/quote"
"go.mongodb.org/mongo-driver/v2/bson"
)
type storageError string
func (e storageError) Error() string {
return string(e)
}
var (
// ErrPaymentNotFound signals that a payment record does not exist.
ErrPaymentNotFound = storageError("payments.storage: payment not found")
// ErrDuplicatePayment signals that idempotency constraints were violated.
ErrDuplicatePayment = storageError("payments.storage: duplicate payment")
// ErrRouteNotFound signals that a payment route record does not exist.
ErrRouteNotFound = storageError("payments.storage: route not found")
// ErrDuplicateRoute signals that a route already exists for the same transition.
ErrDuplicateRoute = storageError("payments.storage: duplicate route")
// ErrPlanTemplateNotFound signals that a plan template record does not exist.
ErrPlanTemplateNotFound = storageError("payments.storage: plan template not found")
// ErrDuplicatePlanTemplate signals that a plan template already exists for the same transition.
ErrDuplicatePlanTemplate = storageError("payments.storage: duplicate plan template")
)
var (
// Deprecated: use quote/storage.ErrQuoteNotFound.
ErrQuoteNotFound = quotestorage.ErrQuoteNotFound
// Deprecated: use quote/storage.ErrDuplicateQuote.
ErrDuplicateQuote = quotestorage.ErrDuplicateQuote
)
// Repository exposes persistence primitives for the payments domain.
type Repository interface {
Ping(ctx context.Context) error
Payments() PaymentsStore
Quotes() quotestorage.QuotesStore
Routes() RoutesStore
PlanTemplates() PlanTemplatesStore
}
// PaymentsStore manages payment lifecycle state.
type PaymentsStore interface {
Create(ctx context.Context, payment *model.Payment) error
Update(ctx context.Context, payment *model.Payment) error
GetByPaymentRef(ctx context.Context, paymentRef string) (*model.Payment, error)
GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.Payment, error)
GetByChainTransferRef(ctx context.Context, transferRef string) (*model.Payment, error)
List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error)
}
// Deprecated: use quote/storage.QuotesStore.
type QuotesStore = quotestorage.QuotesStore
// RoutesStore manages allowed routing transitions.
type RoutesStore interface {
Create(ctx context.Context, route *model.PaymentRoute) error
Update(ctx context.Context, route *model.PaymentRoute) error
GetByID(ctx context.Context, id bson.ObjectID) (*model.PaymentRoute, error)
List(ctx context.Context, filter *model.PaymentRouteFilter) (*model.PaymentRouteList, error)
}
// PlanTemplatesStore manages orchestration plan templates.
type PlanTemplatesStore interface {
Create(ctx context.Context, template *model.PaymentPlanTemplate) error
Update(ctx context.Context, template *model.PaymentPlanTemplate) error
GetByID(ctx context.Context, id bson.ObjectID) (*model.PaymentPlanTemplate, error)
List(ctx context.Context, filter *model.PaymentPlanTemplateFilter) (*model.PaymentPlanTemplateList, error)
}