Orchestrator refactoring + planned amounts
This commit is contained in:
@@ -64,7 +64,7 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -249,8 +249,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -49,7 +49,7 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
@@ -199,8 +199,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -42,8 +42,8 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -199,8 +199,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -157,7 +157,7 @@ require (
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -380,8 +380,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -401,8 +401,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -91,18 +91,27 @@ type PaymentEndpoint struct {
|
||||
}
|
||||
|
||||
type PaymentOperation struct {
|
||||
StepRef string `json:"stepRef,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
StepRef string `json:"stepRef,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Money *PaymentOperationMoney `json:"money,omitempty"`
|
||||
OperationRef string `json:"operationRef,omitempty"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentOperationMoney struct {
|
||||
Planned *PaymentOperationMoneySnapshot `json:"planned,omitempty"`
|
||||
Executed *PaymentOperationMoneySnapshot `json:"executed,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentOperationMoneySnapshot struct {
|
||||
Amount *paymenttypes.Money `json:"amount,omitempty"`
|
||||
ConvertedAmount *paymenttypes.Money `json:"convertedAmount,omitempty"`
|
||||
OperationRef string `json:"operationRef,omitempty"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type paymentQuoteResponse struct {
|
||||
@@ -581,19 +590,20 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOp
|
||||
|
||||
func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
|
||||
operationRef, gateway := operationRefAndGateway(step.GetStepCode(), step.GetRefs())
|
||||
amount := normalizeOperationMoney(toMoney(step.GetExecutedMoney()))
|
||||
convertedAmount := normalizeOperationMoney(toMoney(step.GetConvertedMoney()))
|
||||
plannedAmount := stepPlannedAmount(step)
|
||||
plannedConvertedAmount := stepPlannedConvertedAmount(step)
|
||||
executedAmount := stepExecutedAmount(step)
|
||||
executedConvertedAmount := stepExecutedConvertedAmount(step)
|
||||
op := PaymentOperation{
|
||||
StepRef: step.GetStepRef(),
|
||||
Code: step.GetStepCode(),
|
||||
State: enumJSONName(step.GetState().String()),
|
||||
Label: strings.TrimSpace(step.GetUserLabel()),
|
||||
Amount: amount,
|
||||
ConvertedAmount: convertedAmount,
|
||||
OperationRef: operationRef,
|
||||
Gateway: gateway,
|
||||
StartedAt: timestampAsTime(step.GetStartedAt()),
|
||||
CompletedAt: timestampAsTime(step.GetCompletedAt()),
|
||||
StepRef: step.GetStepRef(),
|
||||
Code: step.GetStepCode(),
|
||||
State: enumJSONName(step.GetState().String()),
|
||||
Label: strings.TrimSpace(step.GetUserLabel()),
|
||||
Money: toOperationMoney(plannedAmount, plannedConvertedAmount, executedAmount, executedConvertedAmount),
|
||||
OperationRef: operationRef,
|
||||
Gateway: gateway,
|
||||
StartedAt: timestampAsTime(step.GetStartedAt()),
|
||||
CompletedAt: timestampAsTime(step.GetCompletedAt()),
|
||||
}
|
||||
failure := step.GetFailure()
|
||||
if failure == nil {
|
||||
@@ -607,6 +617,89 @@ func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
|
||||
return op
|
||||
}
|
||||
|
||||
func stepPlannedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
|
||||
if step == nil {
|
||||
return nil
|
||||
}
|
||||
if money := step.GetMoney(); money != nil {
|
||||
if planned := money.GetPlanned(); planned != nil {
|
||||
if normalized := normalizeOperationMoney(toMoney(planned.GetAmount())); normalized != nil {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepPlannedConvertedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
|
||||
if step == nil {
|
||||
return nil
|
||||
}
|
||||
if money := step.GetMoney(); money != nil {
|
||||
if planned := money.GetPlanned(); planned != nil {
|
||||
if normalized := normalizeOperationMoney(toMoney(planned.GetConvertedAmount())); normalized != nil {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepExecutedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
|
||||
if step == nil {
|
||||
return nil
|
||||
}
|
||||
if money := step.GetMoney(); money != nil {
|
||||
if executed := money.GetExecuted(); executed != nil {
|
||||
if normalized := normalizeOperationMoney(toMoney(executed.GetAmount())); normalized != nil {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stepExecutedConvertedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
|
||||
if step == nil {
|
||||
return nil
|
||||
}
|
||||
if money := step.GetMoney(); money != nil {
|
||||
if executed := money.GetExecuted(); executed != nil {
|
||||
if normalized := normalizeOperationMoney(toMoney(executed.GetConvertedAmount())); normalized != nil {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toOperationMoney(
|
||||
plannedAmount *paymenttypes.Money,
|
||||
plannedConvertedAmount *paymenttypes.Money,
|
||||
executedAmount *paymenttypes.Money,
|
||||
executedConvertedAmount *paymenttypes.Money,
|
||||
) *PaymentOperationMoney {
|
||||
planned := toOperationMoneySnapshot(plannedAmount, plannedConvertedAmount)
|
||||
executed := toOperationMoneySnapshot(executedAmount, executedConvertedAmount)
|
||||
if planned == nil && executed == nil {
|
||||
return nil
|
||||
}
|
||||
return &PaymentOperationMoney{
|
||||
Planned: planned,
|
||||
Executed: executed,
|
||||
}
|
||||
}
|
||||
|
||||
func toOperationMoneySnapshot(amount *paymenttypes.Money, convertedAmount *paymenttypes.Money) *PaymentOperationMoneySnapshot {
|
||||
if amount == nil && convertedAmount == nil {
|
||||
return nil
|
||||
}
|
||||
return &PaymentOperationMoneySnapshot{
|
||||
Amount: amount,
|
||||
ConvertedAmount: convertedAmount,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeOperationMoney(value *paymenttypes.Money) *paymenttypes.Money {
|
||||
if value == nil {
|
||||
return nil
|
||||
|
||||
@@ -343,83 +343,235 @@ func TestToPaymentOperation_MapsAmount(t *testing.T) {
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
})
|
||||
|
||||
if got := op.Amount; got != nil {
|
||||
t.Fatalf("expected nil amount without executed_money, got=%+v", got)
|
||||
}
|
||||
if got := op.ConvertedAmount; got != nil {
|
||||
t.Fatalf("expected no converted_amount for non-fx operation, got=%+v", got)
|
||||
if got := op.Money; got != nil {
|
||||
t.Fatalf("expected nil money payload without step money, got=%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_PrefersExecutedMoney(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-4b",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
ExecutedMoney: &moneyv1.Money{Amount: "99.95", Currency: "EUR"},
|
||||
StepRef: "step-4b",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
Money: &orchestrationv2.StepExecutionMoney{
|
||||
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "88.00", Currency: "EUR"},
|
||||
},
|
||||
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "99.95", Currency: "EUR"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if op.Amount == nil {
|
||||
t.Fatal("expected amount to be mapped")
|
||||
if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil {
|
||||
t.Fatal("expected executed amount to be mapped")
|
||||
}
|
||||
if got, want := op.Amount.Amount, "99.95"; got != want {
|
||||
t.Fatalf("amount.value mismatch: got=%q want=%q", got, want)
|
||||
if got, want := op.Money.Executed.Amount.Amount, "99.95"; got != want {
|
||||
t.Fatalf("executed amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Amount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("amount.currency mismatch: got=%q want=%q", got, want)
|
||||
if got, want := op.Money.Executed.Amount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("executed amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := op.ConvertedAmount; got != nil {
|
||||
t.Fatalf("expected no converted_amount for non-fx operation, got=%+v", got)
|
||||
if op.Money.Planned == nil || op.Money.Planned.Amount == nil {
|
||||
t.Fatal("expected planned amount to be exposed")
|
||||
}
|
||||
if got, want := op.Money.Planned.Amount.Amount, "88.00"; got != want {
|
||||
t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Money.Planned.Amount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("planned amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := op.Money.Executed.ConvertedAmount; got != nil {
|
||||
t.Fatalf("expected no executed converted_amount for non-fx operation, got=%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_UsesPlannedMoneyBeforeExecution(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-4c",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING,
|
||||
Money: &orchestrationv2.StepExecutionMoney{
|
||||
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "77.10", Currency: "USD"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if op.Money == nil || op.Money.Planned == nil || op.Money.Planned.Amount == nil {
|
||||
t.Fatal("expected planned amount from structured planned money")
|
||||
}
|
||||
if got, want := op.Money.Planned.Amount.Amount, "77.10"; got != want {
|
||||
t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Money.Planned.Amount.Currency, "USD"; got != want {
|
||||
t.Fatalf("planned amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := op.Money.Executed; got != nil {
|
||||
t.Fatalf("expected no executed snapshot before execution, got=%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_UsesStructuredMoneyEnvelope(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-4d",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING,
|
||||
Money: &orchestrationv2.StepExecutionMoney{
|
||||
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "66.00", Currency: "USD"},
|
||||
},
|
||||
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "67.00", Currency: "USD"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil {
|
||||
t.Fatal("expected amount from structured executed money")
|
||||
}
|
||||
if got, want := op.Money.Executed.Amount.Amount, "67.00"; got != want {
|
||||
t.Fatalf("executed amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if op.Money.Planned == nil || op.Money.Planned.Amount == nil {
|
||||
t.Fatal("expected planned amount from structured money")
|
||||
}
|
||||
if got, want := op.Money.Planned.Amount.Amount, "66.00"; got != want {
|
||||
t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_MapsFxTwoAmounts(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-5",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
ConvertedMoney: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
StepRef: "step-5",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
Money: &orchestrationv2.StepExecutionMoney{
|
||||
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if got := op.Amount; got != nil {
|
||||
if op.Money == nil || op.Money.Executed == nil {
|
||||
t.Fatal("expected executed snapshot to be mapped")
|
||||
}
|
||||
if got := op.Money.Executed.Amount; got != nil {
|
||||
t.Fatalf("expected nil base amount without executed_money, got=%+v", got)
|
||||
}
|
||||
if op.ConvertedAmount == nil {
|
||||
if op.Money.Executed.ConvertedAmount == nil {
|
||||
t.Fatal("expected fx converted amount to be mapped")
|
||||
}
|
||||
if got, want := op.ConvertedAmount.Amount, "100.00"; got != want {
|
||||
if got, want := op.Money.Executed.ConvertedAmount.Amount, "100.00"; got != want {
|
||||
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.ConvertedAmount.Currency, "EUR"; got != want {
|
||||
if got, want := op.Money.Executed.ConvertedAmount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_UsesPlannedFxAmountsBeforeExecution(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-5b",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING,
|
||||
Money: &orchestrationv2.StepExecutionMoney{
|
||||
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"},
|
||||
ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if op.Money == nil || op.Money.Planned == nil {
|
||||
t.Fatal("expected planned snapshot from structured money")
|
||||
}
|
||||
if got, want := op.Money.Planned.Amount.Amount, "109.50"; got != want {
|
||||
t.Fatalf("base amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Money.Planned.Amount.Currency, "USDT"; got != want {
|
||||
t.Fatalf("base amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if op.Money.Planned.ConvertedAmount == nil {
|
||||
t.Fatal("expected fx converted amount from structured planned money")
|
||||
}
|
||||
if got, want := op.Money.Planned.ConvertedAmount.Amount, "100.00"; got != want {
|
||||
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Money.Planned.ConvertedAmount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := op.Money.Executed; got != nil {
|
||||
t.Fatalf("expected nil executed snapshot before execution, got=%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_UsesStructuredFxMoneyEnvelope(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-5c",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
Money: &orchestrationv2.StepExecutionMoney{
|
||||
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"},
|
||||
ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
},
|
||||
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "110.00", Currency: "USDT"},
|
||||
ConvertedAmount: &moneyv1.Money{Amount: "101.00", Currency: "EUR"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if op.Money == nil || op.Money.Executed == nil || op.Money.Planned == nil {
|
||||
t.Fatal("expected snapshots from structured money")
|
||||
}
|
||||
if got, want := op.Money.Executed.Amount.Amount, "110.00"; got != want {
|
||||
t.Fatalf("amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Money.Executed.ConvertedAmount.Amount, "101.00"; got != want {
|
||||
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if op.Money.Planned.Amount == nil || op.Money.Planned.ConvertedAmount == nil {
|
||||
t.Fatal("expected planned amounts from structured money")
|
||||
}
|
||||
if got, want := op.Money.Planned.Amount.Amount, "109.50"; got != want {
|
||||
t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Money.Planned.ConvertedAmount.Amount, "100.00"; got != want {
|
||||
t.Fatalf("planned converted amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_FxWithExecutedMoney_StillProvidesTwoAmounts(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-6",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
ExecutedMoney: &moneyv1.Money{Amount: "109.50", Currency: "USDT"},
|
||||
ConvertedMoney: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
StepRef: "step-6",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
Money: &orchestrationv2.StepExecutionMoney{
|
||||
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"},
|
||||
ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if op.Amount == nil {
|
||||
if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil {
|
||||
t.Fatal("expected fx base amount to be mapped")
|
||||
}
|
||||
if got, want := op.Amount.Amount, "109.50"; got != want {
|
||||
if got, want := op.Money.Executed.Amount.Amount, "109.50"; got != want {
|
||||
t.Fatalf("base amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Amount.Currency, "USDT"; got != want {
|
||||
if got, want := op.Money.Executed.Amount.Currency, "USDT"; got != want {
|
||||
t.Fatalf("base amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if op.ConvertedAmount == nil {
|
||||
if op.Money.Executed.ConvertedAmount == nil {
|
||||
t.Fatal("expected fx quote amount to be mapped")
|
||||
}
|
||||
if got, want := op.ConvertedAmount.Amount, "100.00"; got != want {
|
||||
if got, want := op.Money.Executed.ConvertedAmount.Amount, "100.00"; got != want {
|
||||
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.ConvertedAmount.Currency, "EUR"; got != want {
|
||||
if got, want := op.Money.Executed.ConvertedAmount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -232,8 +232,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -243,8 +243,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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -46,8 +46,8 @@ require (
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -199,8 +199,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -47,6 +47,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -199,8 +199,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -25,6 +25,6 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -160,8 +160,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
|
||||
@@ -50,6 +50,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -201,8 +201,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -32,12 +32,12 @@ require (
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/consensys/gnark-crypto v0.19.2 // indirect
|
||||
github.com/consensys/gnark-crypto v0.20.0 // indirect
|
||||
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/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // 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
|
||||
@@ -89,7 +89,7 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -40,8 +40,8 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP
|
||||
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80=
|
||||
github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
|
||||
github.com/consensys/gnark-crypto v0.20.0 h1:dJmv2sC9KWV/cNRjMjy2S0h7emfyyX8eSsJzwk0DQzw=
|
||||
github.com/consensys/gnark-crypto v0.20.0/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc=
|
||||
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=
|
||||
@@ -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.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7/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.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho=
|
||||
@@ -344,8 +344,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -47,6 +47,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -199,8 +199,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -25,6 +25,6 @@ require (
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -146,8 +146,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
|
||||
@@ -50,6 +50,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -201,8 +201,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -47,6 +47,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -199,8 +199,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -36,12 +36,12 @@ require (
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/consensys/gnark-crypto v0.19.2 // indirect
|
||||
github.com/consensys/gnark-crypto v0.20.0 // indirect
|
||||
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 v1.8.0 // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect
|
||||
github.com/fbsobreira/go-bip39 v1.2.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
@@ -97,8 +97,8 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // 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
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -42,8 +42,8 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP
|
||||
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80=
|
||||
github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
|
||||
github.com/consensys/gnark-crypto v0.20.0 h1:dJmv2sC9KWV/cNRjMjy2S0h7emfyyX8eSsJzwk0DQzw=
|
||||
github.com/consensys/gnark-crypto v0.20.0/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc=
|
||||
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=
|
||||
@@ -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.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.7/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.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho=
|
||||
@@ -360,8 +360,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -371,10 +371,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-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/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -48,6 +48,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -201,8 +201,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/text v0.35.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -216,8 +216,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -48,6 +48,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -201,8 +201,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -46,7 +46,7 @@ card_gateways:
|
||||
|
||||
# Batch optimizer settings:
|
||||
# - default_mode disables aggregation for unmatched traffic
|
||||
# - crypto rule enables destination aggregation for crypto rail
|
||||
# - crypto rule enables aggregation by operation destination for crypto operations
|
||||
optimizer:
|
||||
aggregation:
|
||||
default_mode: "no_optimization"
|
||||
@@ -54,6 +54,7 @@ optimizer:
|
||||
- id: "crypto_by_destination"
|
||||
priority: 100
|
||||
mode: "merge_by_destination"
|
||||
group_by: "rail_target"
|
||||
match:
|
||||
rail: "CRYPTO"
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ card_gateways:
|
||||
|
||||
# Batch optimizer settings:
|
||||
# - default_mode disables aggregation for unmatched traffic
|
||||
# - crypto rule enables destination aggregation for crypto rail
|
||||
# - crypto rule enables aggregation by operation destination for crypto operations
|
||||
optimizer:
|
||||
aggregation:
|
||||
default_mode: "no_optimization"
|
||||
@@ -54,6 +54,7 @@ optimizer:
|
||||
- id: "crypto_by_destination"
|
||||
priority: 100
|
||||
mode: "merge_by_destination"
|
||||
group_by: "rail_target"
|
||||
match:
|
||||
rail: "CRYPTO"
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ replace github.com/tech/sendico/ledger => ../../ledger
|
||||
replace github.com/tech/sendico/payments/storage => ../storage
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
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
|
||||
@@ -40,6 +39,7 @@ require (
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -63,6 +63,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -202,8 +202,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -54,6 +54,7 @@ func buildBatchOptimizationPolicy(cfg optimizerConfig) psvc.BatchOptimizationPol
|
||||
Enabled: ruleCfg.Enabled,
|
||||
Priority: ruleCfg.Priority,
|
||||
Mode: psvc.BatchOptimizationMode(strings.TrimSpace(ruleCfg.Mode)),
|
||||
GroupBy: psvc.BatchOptimizationGrouping(strings.TrimSpace(ruleCfg.GroupBy)),
|
||||
Match: psvc.BatchOptimizationMatch{
|
||||
Rail: model.ParseRail(ruleCfg.Match.Rail),
|
||||
Providers: cloneTrimmedSlice(ruleCfg.Match.Providers),
|
||||
|
||||
@@ -33,6 +33,7 @@ type aggregationRuleConfig struct {
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
Priority int `yaml:"priority"`
|
||||
Mode string `yaml:"mode"`
|
||||
GroupBy string `yaml:"group_by"`
|
||||
Match aggregationMatchConfig `yaml:"match"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
executionStepMetadataRole = "role"
|
||||
executionStepMetadataStatus = "status"
|
||||
|
||||
executionStepRoleSource = "source"
|
||||
executionStepRoleConsumer = "consumer"
|
||||
executionStepCodeCardPayout = "card_payout"
|
||||
)
|
||||
|
||||
func setExecutionStepRole(step *model.ExecutionStep, role string) {
|
||||
role = strings.ToLower(strings.TrimSpace(role))
|
||||
setExecutionStepMetadata(step, executionStepMetadataRole, role)
|
||||
}
|
||||
|
||||
func setExecutionStepStatus(step *model.ExecutionStep, state model.OperationState) {
|
||||
step.State = state
|
||||
setExecutionStepMetadata(step, executionStepMetadataStatus, string(state))
|
||||
}
|
||||
|
||||
func executionStepRole(step *model.ExecutionStep) string {
|
||||
if step == nil {
|
||||
return ""
|
||||
}
|
||||
if role := strings.TrimSpace(step.Metadata[executionStepMetadataRole]); role != "" {
|
||||
return strings.ToLower(role)
|
||||
}
|
||||
if strings.EqualFold(step.Code, executionStepCodeCardPayout) {
|
||||
return executionStepRoleConsumer
|
||||
}
|
||||
return executionStepRoleSource
|
||||
}
|
||||
|
||||
func isSourceExecutionStep(step *model.ExecutionStep) bool {
|
||||
return executionStepRole(step) == executionStepRoleSource
|
||||
}
|
||||
|
||||
func sourceStepsConfirmed(plan *model.ExecutionPlan) bool {
|
||||
if plan == nil || len(plan.Steps) == 0 {
|
||||
return false
|
||||
}
|
||||
hasSource := false
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil || !isSourceExecutionStep(step) {
|
||||
continue
|
||||
}
|
||||
if step.State == model.OperationStateSkipped {
|
||||
continue
|
||||
}
|
||||
hasSource = true
|
||||
if step.State != model.OperationStateSuccess {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return hasSource
|
||||
}
|
||||
|
||||
func findExecutionStepByTransferRef(plan *model.ExecutionPlan, transferRef string) *model.ExecutionStep {
|
||||
if plan == nil {
|
||||
return nil
|
||||
}
|
||||
transferRef = strings.TrimSpace(transferRef)
|
||||
if transferRef == "" {
|
||||
return nil
|
||||
}
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(step.TransferRef), transferRef) {
|
||||
return step
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateExecutionStepFromTransfer(plan *model.ExecutionPlan, event *chainv1.TransferStatusChangedEvent) *model.ExecutionStep {
|
||||
if plan == nil || event == nil || event.GetTransfer() == nil {
|
||||
return nil
|
||||
}
|
||||
transfer := event.GetTransfer()
|
||||
transferRef := strings.TrimSpace(transfer.GetTransferRef())
|
||||
if transferRef == "" {
|
||||
return nil
|
||||
}
|
||||
if status := executionStepStatusFromTransferStatus(transfer.GetStatus()); status != "" {
|
||||
var updated *model.ExecutionStep
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(step.TransferRef), transferRef) {
|
||||
continue
|
||||
}
|
||||
if step.TransferRef == "" {
|
||||
step.TransferRef = transferRef
|
||||
}
|
||||
setExecutionStepStatus(step, status)
|
||||
if updated == nil {
|
||||
updated = step
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func executionStepStatusFromTransferStatus(status chainv1.TransferStatus) model.OperationState {
|
||||
switch status {
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||
return model.OperationStatePlanned
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||
return model.OperationStateProcessing
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||
return model.OperationStateWaiting
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||
return model.OperationStateSuccess
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||
return model.OperationStateFailed
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return model.OperationStateCancelled
|
||||
|
||||
default:
|
||||
return model.OperationStatePlanned
|
||||
}
|
||||
}
|
||||
|
||||
func setExecutionStepMetadata(step *model.ExecutionStep, key, value string) {
|
||||
if step == nil {
|
||||
return
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
if step.Metadata != nil {
|
||||
delete(step.Metadata, key)
|
||||
if len(step.Metadata) == 0 {
|
||||
step.Metadata = nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if step.Metadata == nil {
|
||||
step.Metadata = map[string]string{}
|
||||
}
|
||||
step.Metadata[key] = value
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
ExecutionStepRoleSource = executionStepRoleSource
|
||||
ExecutionStepRoleConsumer = executionStepRoleConsumer
|
||||
)
|
||||
|
||||
func SetExecutionStepRole(step *model.ExecutionStep, role string) {
|
||||
setExecutionStepRole(step, role)
|
||||
}
|
||||
|
||||
func SetExecutionStepStatus(step *model.ExecutionStep, state model.OperationState) {
|
||||
setExecutionStepStatus(step, state)
|
||||
}
|
||||
|
||||
func ExecutionStepRole(step *model.ExecutionStep) string {
|
||||
return executionStepRole(step)
|
||||
}
|
||||
|
||||
func IsSourceExecutionStep(step *model.ExecutionStep) bool {
|
||||
return isSourceExecutionStep(step)
|
||||
}
|
||||
|
||||
func SourceStepsConfirmed(plan *model.ExecutionPlan) bool {
|
||||
return sourceStepsConfirmed(plan)
|
||||
}
|
||||
|
||||
func FindExecutionStepByTransferRef(plan *model.ExecutionPlan, transferRef string) *model.ExecutionStep {
|
||||
return findExecutionStepByTransferRef(plan, transferRef)
|
||||
}
|
||||
|
||||
func UpdateExecutionStepFromTransfer(plan *model.ExecutionPlan, event *chainv1.TransferStatusChangedEvent) *model.ExecutionStep {
|
||||
return updateExecutionStepFromTransfer(plan, event)
|
||||
}
|
||||
|
||||
func ExecutionStepStatusFromTransferStatus(status chainv1.TransferStatus) model.OperationState {
|
||||
return executionStepStatusFromTransferStatus(status)
|
||||
}
|
||||
|
||||
func SetExecutionStepMetadata(step *model.ExecutionStep, key, value string) {
|
||||
setExecutionStepMetadata(step, key, value)
|
||||
}
|
||||
|
||||
func EnsureExecutionRefs(payment *model.Payment) *model.ExecutionRefs {
|
||||
return ensureExecutionRefs(payment)
|
||||
}
|
||||
|
||||
func ExecutionQuote(
|
||||
payment *model.Payment,
|
||||
quote *sharedv1.PaymentQuote,
|
||||
quoteFromSnapshot func(*model.PaymentQuoteSnapshot) *sharedv1.PaymentQuote,
|
||||
) *sharedv1.PaymentQuote {
|
||||
return executionQuote(payment, quote, quoteFromSnapshot)
|
||||
}
|
||||
|
||||
func EnsureExecutionPlanForPlan(payment *model.Payment, plan *model.PaymentPlan) *model.ExecutionPlan {
|
||||
return ensureExecutionPlanForPlan(payment, plan)
|
||||
}
|
||||
|
||||
func ExecutionPlanComplete(plan *model.ExecutionPlan) bool {
|
||||
return executionPlanComplete(plan)
|
||||
}
|
||||
|
||||
func BlockStepConfirmed(plan *model.PaymentPlan, execPlan *model.ExecutionPlan) bool {
|
||||
return blockStepConfirmed(plan, execPlan)
|
||||
}
|
||||
|
||||
func RoleHintsForStep(plan *model.PaymentPlan, idx int) (*account_role.AccountRole, *account_role.AccountRole) {
|
||||
return roleHintsForStep(plan, idx)
|
||||
}
|
||||
|
||||
func LinkRailObservation(payment *model.Payment, rail model.Rail, referenceID, dependsOn string) {
|
||||
linkRailObservation(payment, rail, referenceID, dependsOn)
|
||||
}
|
||||
|
||||
func PlanStepID(step *model.PaymentStep, idx int) string {
|
||||
return planStepID(step, idx)
|
||||
}
|
||||
|
||||
func DescribePlanStep(step *model.PaymentStep) string {
|
||||
return describePlanStep(step)
|
||||
}
|
||||
|
||||
func PlanStepIdempotencyKey(payment *model.Payment, idx int, step *model.PaymentStep) string {
|
||||
return planStepIdempotencyKey(payment, idx, step)
|
||||
}
|
||||
|
||||
func FailureCodeForStep(step *model.PaymentStep) model.PaymentFailureCode {
|
||||
return failureCodeForStep(step)
|
||||
}
|
||||
|
||||
func ExecutionStepsByCode(plan *model.ExecutionPlan) map[string]*model.ExecutionStep {
|
||||
return executionStepsByCode(plan)
|
||||
}
|
||||
|
||||
func PlanStepsByID(plan *model.PaymentPlan) map[string]*model.PaymentStep {
|
||||
return planStepsByID(plan)
|
||||
}
|
||||
|
||||
func StepDependenciesReady(
|
||||
step *model.PaymentStep,
|
||||
execSteps map[string]*model.ExecutionStep,
|
||||
planSteps map[string]*model.PaymentStep,
|
||||
requireSuccess bool,
|
||||
) (ready bool, waiting bool, blocked bool, err error) {
|
||||
return stepDependenciesReady(step, execSteps, planSteps, requireSuccess)
|
||||
}
|
||||
|
||||
func CardPayoutDependenciesConfirmed(plan *model.PaymentPlan, execPlan *model.ExecutionPlan) bool {
|
||||
return cardPayoutDependenciesConfirmed(plan, execPlan)
|
||||
}
|
||||
|
||||
func AnalyzeExecutionPlan(logger mlogger.Logger, payment *model.Payment) (bool, bool, error) {
|
||||
return analyzeExecutionPlan(logger, payment)
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Liveness string
|
||||
|
||||
const (
|
||||
StepFinal Liveness = "final"
|
||||
StepRunnable Liveness = "runnable"
|
||||
StepBlocked Liveness = "blocked"
|
||||
StepDead Liveness = "dead"
|
||||
)
|
||||
|
||||
func buildPaymentStepIndex(plan *model.PaymentPlan) map[string]*model.PaymentStep {
|
||||
idx := make(map[string]*model.PaymentStep, len(plan.Steps))
|
||||
for _, s := range plan.Steps {
|
||||
idx[s.StepID] = s
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
func buildExecutionStepIndex(plan *model.ExecutionPlan) map[string]*model.ExecutionStep {
|
||||
index := make(map[string]*model.ExecutionStep, len(plan.Steps))
|
||||
for _, s := range plan.Steps {
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
index[s.Code] = s
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func stepLiveness(
|
||||
logger mlogger.Logger,
|
||||
step *model.ExecutionStep,
|
||||
pStepIdx map[string]*model.PaymentStep,
|
||||
eStepIdx map[string]*model.ExecutionStep,
|
||||
) Liveness {
|
||||
|
||||
if step.IsTerminal() {
|
||||
return StepFinal
|
||||
}
|
||||
|
||||
pStep, ok := pStepIdx[step.Code]
|
||||
if !ok {
|
||||
logger.Error("Step missing in payment plan",
|
||||
zap.String("step_id", step.Code),
|
||||
)
|
||||
return StepDead
|
||||
}
|
||||
|
||||
for _, depID := range pStep.DependsOn {
|
||||
dep := eStepIdx[depID]
|
||||
if dep == nil {
|
||||
logger.Warn("Dependency missing in execution plan",
|
||||
zap.String("step_id", step.Code),
|
||||
zap.String("dep_id", depID),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
switch dep.State {
|
||||
case model.OperationStateFailed:
|
||||
return StepDead
|
||||
}
|
||||
}
|
||||
|
||||
allSuccess := true
|
||||
for _, depID := range pStep.DependsOn {
|
||||
dep := eStepIdx[depID]
|
||||
if dep == nil || dep.State != model.OperationStateSuccess {
|
||||
allSuccess = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allSuccess {
|
||||
return StepRunnable
|
||||
}
|
||||
|
||||
return StepBlocked
|
||||
}
|
||||
|
||||
func analyzeExecutionPlan(
|
||||
logger mlogger.Logger,
|
||||
payment *model.Payment,
|
||||
) (bool, bool, error) {
|
||||
|
||||
if payment == nil || payment.ExecutionPlan == nil {
|
||||
return true, false, nil
|
||||
}
|
||||
|
||||
eIdx := buildExecutionStepIndex(payment.ExecutionPlan)
|
||||
pIdx := buildPaymentStepIndex(payment.PaymentPlan)
|
||||
|
||||
hasRunnable := false
|
||||
hasFailed := false
|
||||
var rootErr error
|
||||
|
||||
for _, s := range payment.ExecutionPlan.Steps {
|
||||
live := stepLiveness(logger, s, pIdx, eIdx)
|
||||
|
||||
if live == StepRunnable {
|
||||
hasRunnable = true
|
||||
}
|
||||
|
||||
if s.State == model.OperationStateFailed {
|
||||
hasFailed = true
|
||||
if rootErr == nil && s.Error != "" {
|
||||
rootErr = errors.New(s.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done := !hasRunnable
|
||||
return done, hasFailed, rootErr
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
)
|
||||
|
||||
func ensureExecutionRefs(payment *model.Payment) *model.ExecutionRefs {
|
||||
if payment.Execution == nil {
|
||||
payment.Execution = &model.ExecutionRefs{}
|
||||
}
|
||||
return payment.Execution
|
||||
}
|
||||
|
||||
func executionQuote(
|
||||
payment *model.Payment,
|
||||
quote *sharedv1.PaymentQuote,
|
||||
quoteFromSnapshot func(*model.PaymentQuoteSnapshot) *sharedv1.PaymentQuote,
|
||||
) *sharedv1.PaymentQuote {
|
||||
if quote != nil {
|
||||
return quote
|
||||
}
|
||||
if payment != nil && payment.LastQuote != nil && quoteFromSnapshot != nil {
|
||||
return quoteFromSnapshot(payment.LastQuote)
|
||||
}
|
||||
return &sharedv1.PaymentQuote{}
|
||||
}
|
||||
|
||||
func ensureExecutionPlanForPlan(
|
||||
payment *model.Payment,
|
||||
plan *model.PaymentPlan,
|
||||
) *model.ExecutionPlan {
|
||||
|
||||
if payment.ExecutionPlan != nil {
|
||||
return payment.ExecutionPlan
|
||||
}
|
||||
|
||||
exec := &model.ExecutionPlan{
|
||||
Steps: make([]*model.ExecutionStep, 0, len(plan.Steps)),
|
||||
}
|
||||
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
exec.Steps = append(exec.Steps, &model.ExecutionStep{
|
||||
Code: step.StepID,
|
||||
State: model.OperationStatePlanned,
|
||||
OperationRef: uuid.New().String(),
|
||||
})
|
||||
}
|
||||
|
||||
return exec
|
||||
}
|
||||
|
||||
func executionPlanComplete(plan *model.ExecutionPlan) bool {
|
||||
if plan == nil || len(plan.Steps) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
if step.State == model.OperationStateSkipped {
|
||||
continue
|
||||
}
|
||||
if step.State != model.OperationStateSuccess {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func blockStepConfirmed(plan *model.PaymentPlan, execPlan *model.ExecutionPlan) bool {
|
||||
if plan == nil || execPlan == nil || len(plan.Steps) == 0 {
|
||||
return false
|
||||
}
|
||||
execSteps := executionStepsByCode(execPlan)
|
||||
for idx, step := range plan.Steps {
|
||||
if step == nil || step.Action != discovery.RailOperationBlock {
|
||||
continue
|
||||
}
|
||||
execStep := execSteps[planStepID(step, idx)]
|
||||
if execStep == nil {
|
||||
continue
|
||||
}
|
||||
if execStep.State == model.OperationStateSuccess {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func roleHintsForStep(plan *model.PaymentPlan, idx int) (*account_role.AccountRole, *account_role.AccountRole) {
|
||||
if plan == nil || idx <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
for i := idx - 1; i >= 0; i-- {
|
||||
step := plan.Steps[i]
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
if step.Rail != discovery.RailLedger || step.Action != discovery.RailOperationMove {
|
||||
continue
|
||||
}
|
||||
if step.ToRole != nil && strings.TrimSpace(string(*step.ToRole)) != "" {
|
||||
role := *step.ToRole
|
||||
return &role, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func linkRailObservation(payment *model.Payment, rail model.Rail, referenceID, dependsOn string) {
|
||||
if payment == nil || payment.PaymentPlan == nil {
|
||||
return
|
||||
}
|
||||
ref := strings.TrimSpace(referenceID)
|
||||
if ref == "" {
|
||||
return
|
||||
}
|
||||
plan := payment.PaymentPlan
|
||||
execPlan := ensureExecutionPlanForPlan(payment, plan)
|
||||
if execPlan == nil {
|
||||
return
|
||||
}
|
||||
dep := strings.TrimSpace(dependsOn)
|
||||
for idx, planStep := range plan.Steps {
|
||||
if planStep == nil {
|
||||
continue
|
||||
}
|
||||
if planStep.Rail != rail || planStep.Action != discovery.RailOperationObserveConfirm {
|
||||
continue
|
||||
}
|
||||
if dep != "" {
|
||||
matched := false
|
||||
for _, entry := range planStep.DependsOn {
|
||||
if strings.EqualFold(strings.TrimSpace(entry), dep) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if idx >= len(execPlan.Steps) {
|
||||
continue
|
||||
}
|
||||
execStep := execPlan.Steps[idx]
|
||||
if execStep == nil {
|
||||
execStep = &model.ExecutionStep{Code: planStepID(planStep, idx), Description: describePlanStep(planStep)}
|
||||
execPlan.Steps[idx] = execStep
|
||||
}
|
||||
if execStep.TransferRef == "" {
|
||||
execStep.TransferRef = ref
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func planStepID(step *model.PaymentStep, idx int) string {
|
||||
if step != nil {
|
||||
if val := strings.TrimSpace(step.StepID); val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("plan_step_%d", idx)
|
||||
}
|
||||
|
||||
func describePlanStep(step *model.PaymentStep) string {
|
||||
if step == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf("%s %s", step.Rail, step.Action))
|
||||
}
|
||||
|
||||
func planStepIdempotencyKey(payment *model.Payment, idx int, step *model.PaymentStep) string {
|
||||
base := ""
|
||||
if payment != nil {
|
||||
base = strings.TrimSpace(payment.IdempotencyKey)
|
||||
if base == "" {
|
||||
base = strings.TrimSpace(payment.PaymentRef)
|
||||
}
|
||||
}
|
||||
if base == "" {
|
||||
base = "payment"
|
||||
}
|
||||
if step == nil {
|
||||
return fmt.Sprintf("%s:plan:%d", base, idx)
|
||||
}
|
||||
stepID := strings.TrimSpace(step.StepID)
|
||||
if stepID == "" {
|
||||
stepID = fmt.Sprintf("%d", idx)
|
||||
}
|
||||
return fmt.Sprintf("%s:plan:%s:%s:%s", base, stepID, strings.ToLower(string(step.Rail)), strings.ToLower(string(step.Action)))
|
||||
}
|
||||
|
||||
func failureCodeForStep(step *model.PaymentStep) model.PaymentFailureCode {
|
||||
if step == nil {
|
||||
return model.PaymentFailureCodePolicy
|
||||
}
|
||||
switch step.Rail {
|
||||
case discovery.RailLedger:
|
||||
if step.Action == discovery.RailOperationFXConvert {
|
||||
return model.PaymentFailureCodeFX
|
||||
}
|
||||
return model.PaymentFailureCodeLedger
|
||||
case discovery.RailCrypto:
|
||||
return model.PaymentFailureCodeChain
|
||||
default:
|
||||
return model.PaymentFailureCodePolicy
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
func executionStepsByCode(plan *model.ExecutionPlan) map[string]*model.ExecutionStep {
|
||||
result := map[string]*model.ExecutionStep{}
|
||||
if plan == nil {
|
||||
return result
|
||||
}
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
if code := strings.TrimSpace(step.Code); code != "" {
|
||||
result[code] = step
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func planStepsByID(plan *model.PaymentPlan) map[string]*model.PaymentStep {
|
||||
result := map[string]*model.PaymentStep{}
|
||||
if plan == nil {
|
||||
return result
|
||||
}
|
||||
for idx, step := range plan.Steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
id := planStepID(step, idx)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
result[id] = step
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stepDependenciesReady(
|
||||
step *model.PaymentStep,
|
||||
execSteps map[string]*model.ExecutionStep,
|
||||
planSteps map[string]*model.PaymentStep,
|
||||
requireSuccess bool,
|
||||
) (ready bool, waiting bool, blocked bool, err error) {
|
||||
|
||||
if step == nil {
|
||||
return false, false, false,
|
||||
merrors.InvalidArgument("payment plan: step is required")
|
||||
}
|
||||
|
||||
for _, dep := range step.DependsOn {
|
||||
key := strings.TrimSpace(dep)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
execStep := execSteps[key]
|
||||
if execStep == nil {
|
||||
// step has not been started
|
||||
return false, true, false, nil
|
||||
}
|
||||
|
||||
if execStep.State == model.OperationStateFailed ||
|
||||
execStep.State == model.OperationStateCancelled {
|
||||
// dependency dead, step is impossible
|
||||
return false, false, true, nil
|
||||
}
|
||||
|
||||
if !execStep.ReadyForNext() {
|
||||
// step is processed
|
||||
return false, true, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Commit policies
|
||||
// ------------------------------------------------------------
|
||||
switch step.CommitPolicy {
|
||||
|
||||
case model.CommitPolicyImmediate, model.CommitPolicyUnspecified:
|
||||
return true, false, false, nil
|
||||
|
||||
case model.CommitPolicyAfterSuccess:
|
||||
commitAfter := step.CommitAfter
|
||||
if len(commitAfter) == 0 {
|
||||
commitAfter = step.DependsOn
|
||||
}
|
||||
|
||||
for _, dep := range commitAfter {
|
||||
key := strings.TrimSpace(dep)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
execStep := execSteps[key]
|
||||
if execStep == nil {
|
||||
return false, true, false,
|
||||
merrors.InvalidArgument("commit dependency missing")
|
||||
}
|
||||
|
||||
if execStep.State == model.OperationStateFailed ||
|
||||
execStep.State == model.OperationStateCancelled {
|
||||
return false, false, true, nil
|
||||
}
|
||||
|
||||
if !execStep.IsSuccess() {
|
||||
return false, true, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, false, false, nil
|
||||
|
||||
case model.CommitPolicyAfterFailure:
|
||||
commitAfter := step.CommitAfter
|
||||
if len(commitAfter) == 0 {
|
||||
commitAfter = step.DependsOn
|
||||
}
|
||||
|
||||
for _, dep := range commitAfter {
|
||||
key := strings.TrimSpace(dep)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
execStep := execSteps[key]
|
||||
if execStep == nil {
|
||||
return false, true, false,
|
||||
merrors.InvalidArgument("commit dependency missing")
|
||||
}
|
||||
|
||||
if execStep.State == model.OperationStateFailed {
|
||||
continue
|
||||
}
|
||||
|
||||
if execStep.IsTerminal() {
|
||||
// complete with fail, block
|
||||
return false, false, true, nil
|
||||
}
|
||||
|
||||
// still exexuting, wait
|
||||
return false, true, false, nil
|
||||
}
|
||||
|
||||
return true, false, false, nil
|
||||
|
||||
case model.CommitPolicyAfterCanceled:
|
||||
commitAfter := step.CommitAfter
|
||||
if len(commitAfter) == 0 {
|
||||
commitAfter = step.DependsOn
|
||||
}
|
||||
|
||||
for _, dep := range commitAfter {
|
||||
key := strings.TrimSpace(dep)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
execStep := execSteps[key]
|
||||
if execStep == nil {
|
||||
return false, true, false,
|
||||
merrors.InvalidArgument("commit dependency missing")
|
||||
}
|
||||
|
||||
if !execStep.IsTerminal() {
|
||||
return false, true, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, false, false, nil
|
||||
|
||||
default:
|
||||
return true, false, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func cardPayoutDependenciesConfirmed(
|
||||
plan *model.PaymentPlan,
|
||||
execPlan *model.ExecutionPlan,
|
||||
) bool {
|
||||
|
||||
if execPlan == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if plan == nil || len(plan.Steps) == 0 {
|
||||
return sourceStepsConfirmed(execPlan)
|
||||
}
|
||||
|
||||
execSteps := executionStepsByCode(execPlan)
|
||||
planSteps := planStepsByID(plan)
|
||||
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if step.Rail != discovery.RailCardPayout ||
|
||||
step.Action != discovery.RailOperationSend {
|
||||
continue
|
||||
}
|
||||
|
||||
ready, waiting, blocked, err :=
|
||||
stepDependenciesReady(step, execSteps, planSteps, true)
|
||||
|
||||
if err != nil || blocked {
|
||||
// payout definitely cannot run
|
||||
return false
|
||||
}
|
||||
|
||||
if waiting {
|
||||
// dependencies exist but are not finished yet
|
||||
// payout must NOT run
|
||||
return false
|
||||
}
|
||||
|
||||
// only true when dependencies are REALLY satisfied
|
||||
return ready
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -44,33 +44,37 @@ const (
|
||||
|
||||
// StepShell defines one initial step telemetry item.
|
||||
type StepShell struct {
|
||||
StepRef string `bson:"stepRef" json:"stepRef"`
|
||||
StepCode string `bson:"stepCode" json:"stepCode"`
|
||||
Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"`
|
||||
Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"`
|
||||
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
|
||||
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
|
||||
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
|
||||
StepRef string `bson:"stepRef" json:"stepRef"`
|
||||
StepCode string `bson:"stepCode" json:"stepCode"`
|
||||
Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"`
|
||||
Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"`
|
||||
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
|
||||
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
|
||||
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
|
||||
PlannedMoney *paymenttypes.Money `bson:"plannedMoney,omitempty" json:"plannedMoney,omitempty"`
|
||||
PlannedConvertedMoney *paymenttypes.Money `bson:"plannedConvertedMoney,omitempty" json:"plannedConvertedMoney,omitempty"`
|
||||
}
|
||||
|
||||
// StepExecution is runtime telemetry for one step.
|
||||
type StepExecution struct {
|
||||
StepRef string `bson:"stepRef" json:"stepRef"`
|
||||
StepCode string `bson:"stepCode" json:"stepCode"`
|
||||
Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"`
|
||||
Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"`
|
||||
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
|
||||
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"`
|
||||
ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executedMoney,omitempty"`
|
||||
ConvertedMoney *paymenttypes.Money `bson:"convertedMoney,omitempty" json:"convertedMoney,omitempty"`
|
||||
StepRef string `bson:"stepRef" json:"stepRef"`
|
||||
StepCode string `bson:"stepCode" json:"stepCode"`
|
||||
Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"`
|
||||
Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"`
|
||||
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
|
||||
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"`
|
||||
ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executedMoney,omitempty"`
|
||||
ConvertedMoney *paymenttypes.Money `bson:"convertedMoney,omitempty" json:"convertedMoney,omitempty"`
|
||||
PlannedMoney *paymenttypes.Money `bson:"plannedMoney,omitempty" json:"plannedMoney,omitempty"`
|
||||
PlannedConvertedMoney *paymenttypes.Money `bson:"plannedConvertedMoney,omitempty" json:"plannedConvertedMoney,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalRef links step execution to an external operation.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
@@ -148,15 +149,17 @@ func buildInitialStepTelemetry(shell []StepShell) ([]StepExecution, error) {
|
||||
instanceID := strings.TrimSpace(shell[i].InstanceID)
|
||||
|
||||
out = append(out, StepExecution{
|
||||
StepRef: stepRef,
|
||||
StepCode: stepCode,
|
||||
Rail: railValue,
|
||||
Gateway: gatewayID,
|
||||
InstanceID: instanceID,
|
||||
ReportVisibility: visibility,
|
||||
UserLabel: userLabel,
|
||||
State: StepStatePending,
|
||||
Attempt: 1,
|
||||
StepRef: stepRef,
|
||||
StepCode: stepCode,
|
||||
Rail: railValue,
|
||||
Gateway: gatewayID,
|
||||
InstanceID: instanceID,
|
||||
ReportVisibility: visibility,
|
||||
UserLabel: userLabel,
|
||||
State: StepStatePending,
|
||||
Attempt: 1,
|
||||
PlannedMoney: svcshared.CloneMoneyTrimNonEmpty(shell[i].PlannedMoney),
|
||||
PlannedConvertedMoney: svcshared.CloneMoneyTrimNonEmpty(shell[i].PlannedConvertedMoney),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oshared"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
@@ -123,9 +125,9 @@ func normalizeGatewayEvent(src GatewayEvent) (*normalizedEvent, error) {
|
||||
ev := &normalizedEvent{
|
||||
stepRef: strings.TrimSpace(src.StepRef),
|
||||
targetState: target,
|
||||
failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)),
|
||||
failureInfo: buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)),
|
||||
forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
|
||||
executedMoney: normalizeEventMoney(src.ExecutedMoney),
|
||||
executedMoney: svcshared.CloneMoneyTrimNonEmpty(src.ExecutedMoney),
|
||||
}
|
||||
ev.matchRefs = normalizeRefList([]agg.ExternalRef{
|
||||
{
|
||||
@@ -162,7 +164,7 @@ func normalizeLedgerEvent(src LedgerEvent) (*normalizedEvent, error) {
|
||||
ev := &normalizedEvent{
|
||||
stepRef: strings.TrimSpace(src.StepRef),
|
||||
targetState: target,
|
||||
failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)),
|
||||
failureInfo: buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)),
|
||||
forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
|
||||
}
|
||||
ev.matchRefs = normalizeRefList([]agg.ExternalRef{
|
||||
@@ -194,7 +196,7 @@ func normalizeCardEvent(src CardEvent) (*normalizedEvent, error) {
|
||||
ev := &normalizedEvent{
|
||||
stepRef: strings.TrimSpace(src.StepRef),
|
||||
targetState: target,
|
||||
failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)),
|
||||
failureInfo: buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)),
|
||||
forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
|
||||
}
|
||||
ev.matchRefs = normalizeRefList([]agg.ExternalRef{
|
||||
@@ -268,18 +270,7 @@ func normalizeCardStatus(status CardStatus) (CardStatus, bool) {
|
||||
}
|
||||
|
||||
func normalizeEventMoney(money *paymenttypes.Money) *paymenttypes.Money {
|
||||
if money == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(money.GetAmount())
|
||||
currency := strings.TrimSpace(money.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
return svcshared.CloneMoneyTrimNonEmpty(money)
|
||||
}
|
||||
|
||||
func mapFailureTarget(status any, retryable *bool) (agg.StepState, bool) {
|
||||
@@ -302,14 +293,6 @@ func mapFailureTarget(status any, retryable *bool) (agg.StepState, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -197,3 +197,57 @@ func TestAggregate_UserBatchQuoteSampleCompactsToSingleRecipientOperation(t *tes
|
||||
t.Fatalf("aggregated items mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregate_MergeByRecipient_IgnoresSourceAndQuoteRefs(t *testing.T) {
|
||||
aggregator := New()
|
||||
|
||||
firstIntent := sampleIntent("intent-a", "card-1", "100")
|
||||
secondIntent := sampleIntent("intent-b", "card-1", "125")
|
||||
secondIntent.Source.ManagedWallet.ManagedWalletRef = "src-wallet-2"
|
||||
|
||||
firstQuote := sampleQuote("quote-a", "100", "9150", "1.8")
|
||||
secondQuote := sampleQuote("quote-b", "125", "11437.5", "1.8")
|
||||
secondQuote.Route.RouteRef = "route-b"
|
||||
secondQuote.FXQuote.QuoteRef = "fx-b"
|
||||
secondQuote.FXQuote.RateRef = "rate-b"
|
||||
secondQuote.FXQuote.Price.Value = "92.7"
|
||||
|
||||
in := Input{
|
||||
Items: []Item{
|
||||
{
|
||||
IntentSnapshot: firstIntent,
|
||||
QuoteSnapshot: firstQuote,
|
||||
MergeMode: MergeModeByRecipient,
|
||||
},
|
||||
{
|
||||
IntentSnapshot: secondIntent,
|
||||
QuoteSnapshot: secondQuote,
|
||||
MergeMode: MergeModeByRecipient,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := aggregator.Aggregate(in)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregate returned error: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
t.Fatal("expected output")
|
||||
}
|
||||
if got, want := len(out.Groups), 1; got != want {
|
||||
t.Fatalf("groups count mismatch: got=%d want=%d", got, want)
|
||||
}
|
||||
group := out.Groups[0]
|
||||
if got, want := group.IntentSnapshot.Amount.Amount, "225"; got != want {
|
||||
t.Fatalf("intent amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if group.QuoteSnapshot == nil {
|
||||
t.Fatal("expected quote snapshot")
|
||||
}
|
||||
if got, want := group.QuoteSnapshot.DebitAmount.Amount, "225"; got != want {
|
||||
t.Fatalf("debit amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := group.QuoteSnapshot.ExpectedSettlementAmount.Amount, "20587.5"; got != want {
|
||||
t.Fatalf("settlement amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,33 @@ func cloneStringSlice(src []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneGroupMembers(src []GroupMember) []GroupMember {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]GroupMember, 0, len(src))
|
||||
for i := range src {
|
||||
member := src[i]
|
||||
intentSnapshot, err := cloneIntentSnapshot(member.IntentSnapshot)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
quoteSnapshot, err := cloneQuoteSnapshot(member.QuoteSnapshot)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, GroupMember{
|
||||
IntentRef: strings.TrimSpace(member.IntentRef),
|
||||
IntentSnapshot: intentSnapshot,
|
||||
QuoteSnapshot: quoteSnapshot,
|
||||
})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneIntentSnapshot(src model.PaymentIntent) (model.PaymentIntent, error) {
|
||||
var dst model.PaymentIntent
|
||||
if err := bsonClone(src, &dst); err != nil {
|
||||
|
||||
@@ -95,8 +95,6 @@ func mergeQuoteSnapshot(dst *model.PaymentQuoteSnapshot, src *model.PaymentQuote
|
||||
|
||||
if strings.TrimSpace(dst.QuoteRef) == "" {
|
||||
dst.QuoteRef = strings.TrimSpace(src.QuoteRef)
|
||||
} else if srcRef := strings.TrimSpace(src.QuoteRef); srcRef != "" && dst.QuoteRef != srcRef {
|
||||
return merrors.InvalidArgument("quote_snapshot.quote_ref mismatch")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,7 +3,6 @@ package opagg
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
@@ -14,9 +13,7 @@ func mergeRoute(dst, src *paymenttypes.QuoteRouteSpecification) (*paymenttypes.Q
|
||||
if src == nil {
|
||||
return dst, nil
|
||||
}
|
||||
if routeSignature(dst) != routeSignature(src) {
|
||||
return nil, merrors.InvalidArgument("quote_snapshot.route mismatch")
|
||||
}
|
||||
// Aggregation is destination-driven; keep the first route as execution template.
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
@@ -27,9 +24,6 @@ func mergeFXQuote(dst, src *paymenttypes.FXQuote) (*paymenttypes.FXQuote, error)
|
||||
if src == nil {
|
||||
return dst, nil
|
||||
}
|
||||
if fxQuoteSignature(dst) != fxQuoteSignature(src) {
|
||||
return nil, merrors.InvalidArgument("quote_snapshot.fx_quote mismatch")
|
||||
}
|
||||
var err error
|
||||
dst.BaseAmount, err = mergeMoney(dst.BaseAmount, src.BaseAmount, "quote_snapshot.fx_quote.base_amount")
|
||||
if err != nil {
|
||||
|
||||
@@ -31,15 +31,24 @@ type Item struct {
|
||||
IntentSnapshot model.PaymentIntent
|
||||
QuoteSnapshot *model.PaymentQuoteSnapshot
|
||||
MergeMode MergeMode
|
||||
MergeKey string
|
||||
PolicyTag string
|
||||
}
|
||||
|
||||
// GroupMember preserves one original batch member inside an aggregated group.
|
||||
type GroupMember struct {
|
||||
IntentRef string
|
||||
IntentSnapshot model.PaymentIntent
|
||||
QuoteSnapshot *model.PaymentQuoteSnapshot
|
||||
}
|
||||
|
||||
// Group is one aggregated recipient operation group.
|
||||
type Group struct {
|
||||
RecipientKey string
|
||||
IntentRefs []string
|
||||
IntentSnapshot model.PaymentIntent
|
||||
QuoteSnapshot *model.PaymentQuoteSnapshot
|
||||
Members []GroupMember
|
||||
}
|
||||
|
||||
// Output is the aggregation result.
|
||||
|
||||
@@ -27,6 +27,7 @@ type groupAccumulator struct {
|
||||
intentRefs []string
|
||||
intent model.PaymentIntent
|
||||
quote *model.PaymentQuoteSnapshot
|
||||
members []GroupMember
|
||||
}
|
||||
|
||||
func (s *svc) Aggregate(in Input) (out *Output, err error) {
|
||||
@@ -90,12 +91,23 @@ func (s *svc) Aggregate(in Input) (out *Output, err error) {
|
||||
if quoteSnapshot == nil {
|
||||
return nil, merrors.InvalidArgument("items[" + itoa(i) + "].quote_snapshot is required")
|
||||
}
|
||||
member, memberErr := makeGroupMember(intentRef, item.IntentSnapshot, item.QuoteSnapshot)
|
||||
if memberErr != nil {
|
||||
return nil, memberErr
|
||||
}
|
||||
groupRecipientKey := recipientKey
|
||||
if normalizeMergeMode(item.MergeMode) == MergeModeByRecipient {
|
||||
if override := strings.TrimSpace(item.MergeKey); override != "" {
|
||||
groupRecipientKey = override
|
||||
}
|
||||
}
|
||||
|
||||
groups[key] = &groupAccumulator{
|
||||
recipientKey: recipientKey,
|
||||
recipientKey: groupRecipientKey,
|
||||
intentRefs: []string{intentRef},
|
||||
intent: intentSnapshot,
|
||||
quote: quoteSnapshot,
|
||||
members: []GroupMember{member},
|
||||
}
|
||||
order = append(order, key)
|
||||
continue
|
||||
@@ -108,6 +120,11 @@ func (s *svc) Aggregate(in Input) (out *Output, err error) {
|
||||
return nil, merrors.InvalidArgument("items[" + itoa(i) + "]: " + err.Error())
|
||||
}
|
||||
acc.intentRefs = append(acc.intentRefs, intentRef)
|
||||
member, memberErr := makeGroupMember(intentRef, item.IntentSnapshot, item.QuoteSnapshot)
|
||||
if memberErr != nil {
|
||||
return nil, memberErr
|
||||
}
|
||||
acc.members = append(acc.members, member)
|
||||
}
|
||||
|
||||
out = &Output{
|
||||
@@ -139,6 +156,7 @@ func (s *svc) Aggregate(in Input) (out *Output, err error) {
|
||||
IntentRefs: cloneStringSlice(acc.intentRefs),
|
||||
IntentSnapshot: finalIntent,
|
||||
QuoteSnapshot: finalQuote,
|
||||
Members: cloneGroupMembers(acc.members),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,15 +183,28 @@ func validateItem(item Item) error {
|
||||
}
|
||||
|
||||
func groupingKey(item Item) (string, string, error) {
|
||||
sourceKey, err := endpointKey(item.IntentSnapshot.Source)
|
||||
if err != nil {
|
||||
return "", "", merrors.InvalidArgument("intent_snapshot.source: " + err.Error())
|
||||
}
|
||||
recipientKey, err := endpointKey(item.IntentSnapshot.Destination)
|
||||
if err != nil {
|
||||
return "", "", merrors.InvalidArgument("intent_snapshot.destination: " + err.Error())
|
||||
}
|
||||
|
||||
if normalizeMergeMode(item.MergeMode) == MergeModeByRecipient {
|
||||
mergeKey := strings.TrimSpace(item.MergeKey)
|
||||
if mergeKey == "" {
|
||||
mergeKey = recipientKey
|
||||
}
|
||||
key := strings.Join([]string{
|
||||
"kind=" + strings.ToLower(strings.TrimSpace(string(item.IntentSnapshot.Kind))),
|
||||
"merge_key=" + mergeKey,
|
||||
}, keySep)
|
||||
return key, recipientKey, nil
|
||||
}
|
||||
|
||||
sourceKey, err := endpointKey(item.IntentSnapshot.Source)
|
||||
if err != nil {
|
||||
return "", "", merrors.InvalidArgument("intent_snapshot.source: " + err.Error())
|
||||
}
|
||||
|
||||
quote := item.QuoteSnapshot
|
||||
key := strings.Join([]string{
|
||||
"kind=" + strings.ToLower(strings.TrimSpace(string(item.IntentSnapshot.Kind))),
|
||||
@@ -190,6 +221,22 @@ func groupingKey(item Item) (string, string, error) {
|
||||
return key, recipientKey, nil
|
||||
}
|
||||
|
||||
func makeGroupMember(intentRef string, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) (GroupMember, error) {
|
||||
intentSnapshot, err := cloneIntentSnapshot(intent)
|
||||
if err != nil {
|
||||
return GroupMember{}, err
|
||||
}
|
||||
quoteSnapshot, err := cloneQuoteSnapshot(quote)
|
||||
if err != nil {
|
||||
return GroupMember{}, err
|
||||
}
|
||||
return GroupMember{
|
||||
IntentRef: strings.TrimSpace(intentRef),
|
||||
IntentSnapshot: intentSnapshot,
|
||||
QuoteSnapshot: quoteSnapshot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isBatchingEligible(quote *model.PaymentQuoteSnapshot) bool {
|
||||
if quote == nil || quote.ExecutionConditions == nil {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package oshared
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
)
|
||||
|
||||
func CloneExternalRefs(refs []agg.ExternalRef) []agg.ExternalRef {
|
||||
if len(refs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]agg.ExternalRef, 0, len(refs))
|
||||
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)
|
||||
out = append(out, ref)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CloneTimeUTC(ts *time.Time) *time.Time {
|
||||
if ts == nil {
|
||||
return nil
|
||||
}
|
||||
val := ts.UTC()
|
||||
return &val
|
||||
}
|
||||
@@ -2,14 +2,14 @@ package prepo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oshared"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
pm "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
@@ -108,50 +108,14 @@ func cloneStepExecutions(src []agg.StepExecution) []agg.StepExecution {
|
||||
if step.Attempt == 0 {
|
||||
step.Attempt = 1
|
||||
}
|
||||
step.ExternalRefs = cloneExternalRefs(step.ExternalRefs)
|
||||
step.ExecutedMoney = cloneStepMoney(step.ExecutedMoney)
|
||||
step.ConvertedMoney = cloneStepMoney(step.ConvertedMoney)
|
||||
step.StartedAt = cloneTime(step.StartedAt)
|
||||
step.CompletedAt = cloneTime(step.CompletedAt)
|
||||
step.ExternalRefs = oshared.CloneExternalRefs(step.ExternalRefs)
|
||||
step.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(step.ExecutedMoney)
|
||||
step.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(step.ConvertedMoney)
|
||||
step.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(step.PlannedMoney)
|
||||
step.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(step.PlannedConvertedMoney)
|
||||
step.StartedAt = oshared.CloneTimeUTC(step.StartedAt)
|
||||
step.CompletedAt = oshared.CloneTimeUTC(step.CompletedAt)
|
||||
out = append(out, step)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneStepMoney(money *paymenttypes.Money) *paymenttypes.Money {
|
||||
if money == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(money.GetAmount())
|
||||
currency := strings.TrimSpace(money.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneExternalRefs(refs []agg.ExternalRef) []agg.ExternalRef {
|
||||
if len(refs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]agg.ExternalRef, 0, len(refs))
|
||||
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)
|
||||
out = append(out, ref)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTime(ts *time.Time) *time.Time {
|
||||
if ts == nil {
|
||||
return nil
|
||||
}
|
||||
val := ts.UTC()
|
||||
return &val
|
||||
}
|
||||
|
||||
@@ -118,23 +118,26 @@ func TestMap_Success(t *testing.T) {
|
||||
if got, want := steps[0].GetUserLabel(), "Card payout"; got != want {
|
||||
t.Fatalf("user_label mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if steps[0].GetExecutedMoney() == nil {
|
||||
t.Fatal("expected executed_money to be mapped")
|
||||
if steps[0].GetMoney() == nil {
|
||||
t.Fatal("expected structured money envelope to be mapped")
|
||||
}
|
||||
if got, want := steps[0].GetExecutedMoney().GetAmount(), "95"; got != want {
|
||||
t.Fatalf("executed_money.amount mismatch: got=%q want=%q", got, want)
|
||||
if steps[0].GetMoney().GetPlanned() == nil {
|
||||
t.Fatal("expected structured planned money snapshot")
|
||||
}
|
||||
if got, want := steps[0].GetExecutedMoney().GetCurrency(), "USD"; got != want {
|
||||
t.Fatalf("executed_money.currency mismatch: got=%q want=%q", got, want)
|
||||
if got, want := steps[0].GetMoney().GetPlanned().GetAmount().GetAmount(), "96"; got != want {
|
||||
t.Fatalf("money.planned.amount.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if steps[0].GetConvertedMoney() == nil {
|
||||
t.Fatal("expected converted_money to be mapped")
|
||||
if got, want := steps[0].GetMoney().GetPlanned().GetConvertedAmount().GetAmount(), "91"; got != want {
|
||||
t.Fatalf("money.planned.converted_amount.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[0].GetConvertedMoney().GetAmount(), "90"; got != want {
|
||||
t.Fatalf("converted_money.amount mismatch: got=%q want=%q", got, want)
|
||||
if steps[0].GetMoney().GetExecuted() == nil {
|
||||
t.Fatal("expected structured executed money snapshot")
|
||||
}
|
||||
if got, want := steps[0].GetConvertedMoney().GetCurrency(), "EUR"; got != want {
|
||||
t.Fatalf("converted_money.currency mismatch: got=%q want=%q", got, want)
|
||||
if got, want := steps[0].GetMoney().GetExecuted().GetAmount().GetAmount(), "95"; got != want {
|
||||
t.Fatalf("money.executed.amount.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[0].GetMoney().GetExecuted().GetConvertedAmount().GetAmount(), "90"; got != want {
|
||||
t.Fatalf("money.executed.converted_amount.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[1].GetReportVisibility(), orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN; got != want {
|
||||
t.Fatalf("report_visibility mismatch: got=%s want=%s", got.String(), want.String())
|
||||
@@ -389,6 +392,14 @@ func newPaymentFixture() *agg.Payment {
|
||||
Amount: "90",
|
||||
Currency: "EUR",
|
||||
},
|
||||
PlannedMoney: &paymenttypes.Money{
|
||||
Amount: "96",
|
||||
Currency: "USD",
|
||||
},
|
||||
PlannedConvertedMoney: &paymenttypes.Money{
|
||||
Amount: "91",
|
||||
Currency: "EUR",
|
||||
},
|
||||
StartedAt: &startedAt,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
)
|
||||
|
||||
@@ -35,6 +36,12 @@ func mapStepExecution(step agg.StepExecution, index int) (*orchestrationv2.StepE
|
||||
attempt = 1
|
||||
}
|
||||
|
||||
plannedAmount := moneyToProto(step.PlannedMoney)
|
||||
plannedConverted := moneyToProto(step.PlannedConvertedMoney)
|
||||
executedAmount := moneyToProto(step.ExecutedMoney)
|
||||
executedConverted := moneyToProto(step.ConvertedMoney)
|
||||
moneyEnvelope := mapStepMoneyEnvelope(plannedAmount, plannedConverted, executedAmount, executedConverted)
|
||||
|
||||
return &orchestrationv2.StepExecution{
|
||||
StepRef: strings.TrimSpace(step.StepRef),
|
||||
StepCode: strings.TrimSpace(step.StepCode),
|
||||
@@ -46,11 +53,37 @@ func mapStepExecution(step agg.StepExecution, index int) (*orchestrationv2.StepE
|
||||
Refs: mapExternalRefs(step.StepCode, step.ExternalRefs),
|
||||
ReportVisibility: mapReportVisibility(step.ReportVisibility),
|
||||
UserLabel: strings.TrimSpace(step.UserLabel),
|
||||
ExecutedMoney: moneyToProto(step.ExecutedMoney),
|
||||
ConvertedMoney: moneyToProto(step.ConvertedMoney),
|
||||
Money: moneyEnvelope,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapStepMoneyEnvelope(
|
||||
plannedAmount *moneyv1.Money,
|
||||
plannedConverted *moneyv1.Money,
|
||||
executedAmount *moneyv1.Money,
|
||||
executedConverted *moneyv1.Money,
|
||||
) *orchestrationv2.StepExecutionMoney {
|
||||
planned := mapStepMoneySnapshot(plannedAmount, plannedConverted)
|
||||
executed := mapStepMoneySnapshot(executedAmount, executedConverted)
|
||||
if planned == nil && executed == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestrationv2.StepExecutionMoney{
|
||||
Planned: planned,
|
||||
Executed: executed,
|
||||
}
|
||||
}
|
||||
|
||||
func mapStepMoneySnapshot(amount *moneyv1.Money, converted *moneyv1.Money) *orchestrationv2.StepExecutionMoneySnapshot {
|
||||
if amount == nil && converted == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestrationv2.StepExecutionMoneySnapshot{
|
||||
Amount: amount,
|
||||
ConvertedAmount: converted,
|
||||
}
|
||||
}
|
||||
|
||||
func mapStepFailure(step agg.StepExecution, state agg.StepState) *orchestrationv2.Failure {
|
||||
if state != agg.StepStateFailed && state != agg.StepStateNeedsAttention {
|
||||
return nil
|
||||
|
||||
@@ -30,17 +30,24 @@ type BatchOptimizer interface {
|
||||
Optimize(in BatchOptimizeInput) (*BatchOptimizeOutput, error)
|
||||
}
|
||||
|
||||
// BatchOptimizationMergeKeyResolver allows external packages to resolve custom merge keys.
|
||||
type BatchOptimizationMergeKeyResolver interface {
|
||||
ResolveMergeKey(item qsnap.ResolvedItem, selection BatchOptimizationSelection) (string, bool)
|
||||
}
|
||||
|
||||
// PolicyBatchOptimizerDependencies configures policy-based optimizer implementation.
|
||||
type PolicyBatchOptimizerDependencies struct {
|
||||
Logger mlogger.Logger
|
||||
Aggregator opagg.Aggregator
|
||||
Policy BatchOptimizationPolicy
|
||||
Logger mlogger.Logger
|
||||
Aggregator opagg.Aggregator
|
||||
Policy BatchOptimizationPolicy
|
||||
MergeKeyResolver BatchOptimizationMergeKeyResolver
|
||||
}
|
||||
|
||||
type policyBatchOptimizer struct {
|
||||
logger mlogger.Logger
|
||||
aggregator opagg.Aggregator
|
||||
policy BatchOptimizationPolicy
|
||||
logger mlogger.Logger
|
||||
aggregator opagg.Aggregator
|
||||
policy BatchOptimizationPolicy
|
||||
mergeKeyResolver BatchOptimizationMergeKeyResolver
|
||||
}
|
||||
|
||||
// NewPolicyBatchOptimizer creates default rule-based optimizer implementation.
|
||||
@@ -54,9 +61,10 @@ func NewPolicyBatchOptimizer(deps PolicyBatchOptimizerDependencies) BatchOptimiz
|
||||
aggregator = opagg.New(opagg.Dependencies{Logger: logger})
|
||||
}
|
||||
return &policyBatchOptimizer{
|
||||
logger: logger.Named("batch_optimizer"),
|
||||
aggregator: aggregator,
|
||||
policy: normalizeBatchOptimizationPolicy(deps.Policy),
|
||||
logger: logger.Named("batch_optimizer"),
|
||||
aggregator: aggregator,
|
||||
policy: normalizeBatchOptimizationPolicy(deps.Policy),
|
||||
mergeKeyResolver: deps.MergeKeyResolver,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +76,15 @@ func (o *policyBatchOptimizer) Optimize(in BatchOptimizeInput) (*BatchOptimizeOu
|
||||
aggItems := make([]opagg.Item, 0, len(in.Items))
|
||||
for i := range in.Items {
|
||||
item := in.Items[i]
|
||||
selection := o.policy.Select(batchOptimizationContextFromItem(item))
|
||||
selection := o.policy.SelectMany(batchOptimizationContextsFromItem(item))
|
||||
intentSnapshot := applyBatchOptimizationSelection(item.IntentSnapshot, selection)
|
||||
mergeKey := batchOptimizationMergeKey(item, selection, o.mergeKeyResolver)
|
||||
aggItems = append(aggItems, opagg.Item{
|
||||
IntentRef: item.IntentRef,
|
||||
IntentSnapshot: intentSnapshot,
|
||||
QuoteSnapshot: item.QuoteSnapshot,
|
||||
MergeMode: mergeModeFromBatchOptimization(selection.Mode),
|
||||
MergeKey: mergeKey,
|
||||
PolicyTag: batchOptimizationSelectionTag(selection),
|
||||
})
|
||||
}
|
||||
@@ -89,13 +99,211 @@ func (o *policyBatchOptimizer) Optimize(in BatchOptimizeInput) (*BatchOptimizeOu
|
||||
return &BatchOptimizeOutput{Groups: aggOutput.Groups}, nil
|
||||
}
|
||||
|
||||
func batchOptimizationContextsFromItem(item qsnap.ResolvedItem) []BatchOptimizationContext {
|
||||
out := make([]BatchOptimizationContext, 0, 6)
|
||||
add := func(base BatchOptimizationContext) {
|
||||
if base.Rail == discovery.RailUnspecified {
|
||||
return
|
||||
}
|
||||
money := selectSettlementMoney(item)
|
||||
if money != nil {
|
||||
base.Amount = strings.TrimSpace(money.Amount)
|
||||
base.Currency = strings.TrimSpace(money.Currency)
|
||||
}
|
||||
out = append(out, base)
|
||||
}
|
||||
|
||||
add(batchOptimizationContextFromItem(item))
|
||||
add(mergeBatchOptimizationContext(
|
||||
endpointOptimizationContext(item.IntentSnapshot.Source),
|
||||
sourceHopContext(item.QuoteSnapshot),
|
||||
))
|
||||
route := item.QuoteSnapshot
|
||||
if route != nil && route.Route != nil {
|
||||
if parsed := model.ParseRail(route.Route.Rail); parsed != discovery.RailUnspecified {
|
||||
add(BatchOptimizationContext{
|
||||
Rail: parsed,
|
||||
Provider: strings.TrimSpace(route.Route.Provider),
|
||||
Network: strings.TrimSpace(route.Route.Network),
|
||||
})
|
||||
}
|
||||
for i := range route.Route.Hops {
|
||||
hop := route.Route.Hops[i]
|
||||
if hop == nil {
|
||||
continue
|
||||
}
|
||||
parsed := model.ParseRail(hop.Rail)
|
||||
if parsed == discovery.RailUnspecified {
|
||||
continue
|
||||
}
|
||||
add(BatchOptimizationContext{
|
||||
Rail: parsed,
|
||||
Provider: strings.TrimSpace(hop.Gateway),
|
||||
Network: strings.TrimSpace(hop.Network),
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
add(batchOptimizationContextFromItem(item))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func batchOptimizationMergeKey(
|
||||
item qsnap.ResolvedItem,
|
||||
sel BatchOptimizationSelection,
|
||||
resolver BatchOptimizationMergeKeyResolver,
|
||||
) string {
|
||||
if mergeModeFromBatchOptimization(sel.Mode) != opagg.MergeModeByRecipient {
|
||||
return ""
|
||||
}
|
||||
|
||||
groupBy := normalizeBatchOptimizationGrouping(sel.GroupBy)
|
||||
if groupBy == BatchOptimizationGroupingUnspecified {
|
||||
groupBy = BatchOptimizationGroupingOperationDestination
|
||||
}
|
||||
switch groupBy {
|
||||
case BatchOptimizationGroupingOperationSource:
|
||||
return mergeEndpointKey(item.IntentSnapshot.Source)
|
||||
case BatchOptimizationGroupingRailTarget:
|
||||
if resolver != nil {
|
||||
if key, ok := resolver.ResolveMergeKey(item, sel); ok {
|
||||
return strings.TrimSpace(key)
|
||||
}
|
||||
}
|
||||
return railTargetMergeKey(item, sel.MatchRail)
|
||||
default:
|
||||
return mergeEndpointKey(item.IntentSnapshot.Destination)
|
||||
}
|
||||
}
|
||||
|
||||
func railTargetMergeKey(item qsnap.ResolvedItem, rail model.Rail) string {
|
||||
matchRail := model.ParseRail(string(rail))
|
||||
if matchRail == discovery.RailUnspecified {
|
||||
ctx := batchOptimizationContextFromItem(item)
|
||||
matchRail = ctx.Rail
|
||||
}
|
||||
|
||||
switch matchRail {
|
||||
case discovery.RailCardPayout:
|
||||
return mergeEndpointKey(item.IntentSnapshot.Destination)
|
||||
case discovery.RailCrypto:
|
||||
if endpointRail(item.IntentSnapshot.Destination) == discovery.RailCrypto {
|
||||
return mergeEndpointKey(item.IntentSnapshot.Destination)
|
||||
}
|
||||
if endpointRail(item.IntentSnapshot.Source) == discovery.RailCrypto {
|
||||
return mergeEndpointKey(item.IntentSnapshot.Source)
|
||||
}
|
||||
return routeRailTargetKey(item.QuoteSnapshot, matchRail)
|
||||
default:
|
||||
return routeRailTargetKey(item.QuoteSnapshot, matchRail)
|
||||
}
|
||||
}
|
||||
|
||||
func routeRailTargetKey(snapshot *model.PaymentQuoteSnapshot, rail model.Rail) string {
|
||||
if snapshot == nil || snapshot.Route == nil {
|
||||
return ""
|
||||
}
|
||||
route := snapshot.Route
|
||||
target := ""
|
||||
fallback := ""
|
||||
for i := range route.Hops {
|
||||
hop := route.Hops[i]
|
||||
if hop == nil || model.ParseRail(hop.Rail) != rail {
|
||||
continue
|
||||
}
|
||||
key := strings.Join([]string{
|
||||
"rail=" + strings.ToUpper(strings.TrimSpace(hop.Rail)),
|
||||
"gateway=" + strings.TrimSpace(hop.Gateway),
|
||||
"instance=" + strings.TrimSpace(hop.InstanceID),
|
||||
"network=" + strings.TrimSpace(hop.Network),
|
||||
}, "|")
|
||||
if fallback == "" {
|
||||
fallback = key
|
||||
}
|
||||
if hop.Role == paymenttypes.QuoteRouteHopRoleDestination {
|
||||
target = key
|
||||
break
|
||||
}
|
||||
}
|
||||
if target != "" {
|
||||
return target
|
||||
}
|
||||
if fallback != "" {
|
||||
return fallback
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mergeEndpointKey(ep model.PaymentEndpoint) string {
|
||||
switch normalizeEndpointType(ep) {
|
||||
case model.EndpointTypeLedger:
|
||||
if ep.Ledger == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"type=ledger",
|
||||
"account=" + strings.TrimSpace(ep.Ledger.LedgerAccountRef),
|
||||
"contra=" + strings.TrimSpace(ep.Ledger.ContraLedgerAccountRef),
|
||||
}, "|")
|
||||
case model.EndpointTypeManagedWallet:
|
||||
if ep.ManagedWallet == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"type=managed_wallet",
|
||||
"wallet=" + strings.TrimSpace(ep.ManagedWallet.ManagedWalletRef),
|
||||
"asset=" + mergeAssetKey(ep.ManagedWallet.Asset),
|
||||
}, "|")
|
||||
case model.EndpointTypeExternalChain:
|
||||
if ep.ExternalChain == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"type=external_chain",
|
||||
"address=" + strings.TrimSpace(ep.ExternalChain.Address),
|
||||
"memo=" + strings.TrimSpace(ep.ExternalChain.Memo),
|
||||
"asset=" + mergeAssetKey(ep.ExternalChain.Asset),
|
||||
}, "|")
|
||||
case model.EndpointTypeCard:
|
||||
if ep.Card == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"type=card",
|
||||
"token=" + strings.TrimSpace(ep.Card.Token),
|
||||
"pan=" + strings.TrimSpace(ep.Card.Pan),
|
||||
"masked=" + strings.TrimSpace(ep.Card.MaskedPan),
|
||||
"country=" + strings.TrimSpace(ep.Card.Country),
|
||||
"exp=" + strconv.FormatUint(uint64(ep.Card.ExpMonth), 10) + "-" + strconv.FormatUint(uint64(ep.Card.ExpYear), 10),
|
||||
}, "|")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func mergeAssetKey(asset *paymenttypes.Asset) string {
|
||||
if asset == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{
|
||||
strings.ToUpper(strings.TrimSpace(asset.Chain)),
|
||||
strings.ToUpper(strings.TrimSpace(asset.TokenSymbol)),
|
||||
strings.TrimSpace(asset.ContractAddress),
|
||||
}, ":")
|
||||
}
|
||||
|
||||
func batchOptimizationContextFromItem(item qsnap.ResolvedItem) BatchOptimizationContext {
|
||||
rail, provider, network := destinationHopSignature(item.QuoteSnapshot)
|
||||
// Match policy against operation target (intent destination), not route leg semantics.
|
||||
target := mergeBatchOptimizationContext(
|
||||
endpointOptimizationContext(item.IntentSnapshot.Destination),
|
||||
destinationHopContext(item.QuoteSnapshot),
|
||||
)
|
||||
money := selectSettlementMoney(item)
|
||||
ctx := BatchOptimizationContext{
|
||||
Rail: rail,
|
||||
Provider: provider,
|
||||
Network: network,
|
||||
Rail: target.Rail,
|
||||
Provider: target.Provider,
|
||||
Network: target.Network,
|
||||
}
|
||||
if money != nil {
|
||||
ctx.Amount = strings.TrimSpace(money.Amount)
|
||||
@@ -104,6 +312,70 @@ func batchOptimizationContextFromItem(item qsnap.ResolvedItem) BatchOptimization
|
||||
return ctx
|
||||
}
|
||||
|
||||
func mergeBatchOptimizationContext(primary, fallback BatchOptimizationContext) BatchOptimizationContext {
|
||||
out := primary
|
||||
if out.Rail == "" || out.Rail == discovery.RailUnspecified {
|
||||
out.Rail = fallback.Rail
|
||||
}
|
||||
if strings.TrimSpace(out.Provider) == "" {
|
||||
out.Provider = fallback.Provider
|
||||
}
|
||||
if strings.TrimSpace(out.Network) == "" {
|
||||
out.Network = fallback.Network
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func endpointOptimizationContext(endpoint model.PaymentEndpoint) BatchOptimizationContext {
|
||||
out := BatchOptimizationContext{
|
||||
Rail: endpointRail(endpoint),
|
||||
}
|
||||
switch normalizeEndpointType(endpoint) {
|
||||
case model.EndpointTypeManagedWallet:
|
||||
if endpoint.ManagedWallet != nil && endpoint.ManagedWallet.Asset != nil {
|
||||
out.Network = strings.TrimSpace(endpoint.ManagedWallet.Asset.Chain)
|
||||
}
|
||||
case model.EndpointTypeExternalChain:
|
||||
if endpoint.ExternalChain != nil {
|
||||
if endpoint.ExternalChain.Asset != nil {
|
||||
out.Network = strings.TrimSpace(endpoint.ExternalChain.Asset.Chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func endpointRail(endpoint model.PaymentEndpoint) model.Rail {
|
||||
switch normalizeEndpointType(endpoint) {
|
||||
case model.EndpointTypeManagedWallet, model.EndpointTypeExternalChain:
|
||||
return discovery.RailCrypto
|
||||
case model.EndpointTypeCard:
|
||||
return discovery.RailCardPayout
|
||||
case model.EndpointTypeLedger:
|
||||
return discovery.RailLedger
|
||||
default:
|
||||
return discovery.RailUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeEndpointType(endpoint model.PaymentEndpoint) model.PaymentEndpointType {
|
||||
if endpoint.Type != model.EndpointTypeUnspecified {
|
||||
return endpoint.Type
|
||||
}
|
||||
switch {
|
||||
case endpoint.Ledger != nil:
|
||||
return model.EndpointTypeLedger
|
||||
case endpoint.ManagedWallet != nil:
|
||||
return model.EndpointTypeManagedWallet
|
||||
case endpoint.ExternalChain != nil:
|
||||
return model.EndpointTypeExternalChain
|
||||
case endpoint.Card != nil:
|
||||
return model.EndpointTypeCard
|
||||
default:
|
||||
return model.EndpointTypeUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func selectSettlementMoney(item qsnap.ResolvedItem) *paymenttypes.Money {
|
||||
if item.QuoteSnapshot != nil && item.QuoteSnapshot.ExpectedSettlementAmount != nil {
|
||||
return item.QuoteSnapshot.ExpectedSettlementAmount
|
||||
@@ -111,14 +383,16 @@ func selectSettlementMoney(item qsnap.ResolvedItem) *paymenttypes.Money {
|
||||
return item.IntentSnapshot.Amount
|
||||
}
|
||||
|
||||
func destinationHopSignature(snapshot *model.PaymentQuoteSnapshot) (model.Rail, string, string) {
|
||||
func destinationHopContext(snapshot *model.PaymentQuoteSnapshot) BatchOptimizationContext {
|
||||
if snapshot == nil || snapshot.Route == nil {
|
||||
return discovery.RailUnspecified, "", ""
|
||||
return BatchOptimizationContext{}
|
||||
}
|
||||
route := snapshot.Route
|
||||
rail := model.ParseRail(route.Rail)
|
||||
provider := strings.TrimSpace(route.Provider)
|
||||
network := strings.TrimSpace(route.Network)
|
||||
ctx := BatchOptimizationContext{
|
||||
Rail: model.ParseRail(route.Rail),
|
||||
Provider: strings.TrimSpace(route.Provider),
|
||||
Network: strings.TrimSpace(route.Network),
|
||||
}
|
||||
|
||||
var destinationHop *paymenttypes.QuoteRouteHop
|
||||
for i := range route.Hops {
|
||||
@@ -133,16 +407,54 @@ func destinationHopSignature(snapshot *model.PaymentQuoteSnapshot) (model.Rail,
|
||||
}
|
||||
if destinationHop != nil {
|
||||
if parsed := model.ParseRail(destinationHop.Rail); parsed != discovery.RailUnspecified {
|
||||
rail = parsed
|
||||
ctx.Rail = parsed
|
||||
}
|
||||
if provider == "" {
|
||||
provider = strings.TrimSpace(destinationHop.Gateway)
|
||||
if ctx.Provider == "" {
|
||||
ctx.Provider = strings.TrimSpace(destinationHop.Gateway)
|
||||
}
|
||||
if network == "" {
|
||||
network = strings.TrimSpace(destinationHop.Network)
|
||||
if ctx.Network == "" {
|
||||
ctx.Network = strings.TrimSpace(destinationHop.Network)
|
||||
}
|
||||
}
|
||||
return rail, provider, network
|
||||
return ctx
|
||||
}
|
||||
|
||||
func sourceHopContext(snapshot *model.PaymentQuoteSnapshot) BatchOptimizationContext {
|
||||
if snapshot == nil || snapshot.Route == nil {
|
||||
return BatchOptimizationContext{}
|
||||
}
|
||||
route := snapshot.Route
|
||||
ctx := BatchOptimizationContext{
|
||||
Rail: model.ParseRail(route.Rail),
|
||||
Provider: strings.TrimSpace(route.Provider),
|
||||
Network: strings.TrimSpace(route.Network),
|
||||
}
|
||||
var sourceHop *paymenttypes.QuoteRouteHop
|
||||
for i := range route.Hops {
|
||||
hop := route.Hops[i]
|
||||
if hop == nil {
|
||||
continue
|
||||
}
|
||||
if sourceHop == nil {
|
||||
sourceHop = hop
|
||||
}
|
||||
if hop.Role == paymenttypes.QuoteRouteHopRoleSource {
|
||||
sourceHop = hop
|
||||
break
|
||||
}
|
||||
}
|
||||
if sourceHop != nil {
|
||||
if parsed := model.ParseRail(sourceHop.Rail); parsed != discovery.RailUnspecified {
|
||||
ctx.Rail = parsed
|
||||
}
|
||||
if ctx.Provider == "" {
|
||||
ctx.Provider = strings.TrimSpace(sourceHop.Gateway)
|
||||
}
|
||||
if ctx.Network == "" {
|
||||
ctx.Network = strings.TrimSpace(sourceHop.Network)
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func mergeModeFromBatchOptimization(mode BatchOptimizationMode) opagg.MergeMode {
|
||||
@@ -174,6 +486,12 @@ func applyBatchOptimizationSelection(intent model.PaymentIntent, sel BatchOptimi
|
||||
}
|
||||
intent.Attributes[attrBatchOptimizerMode] = string(mode)
|
||||
intent.Attributes[attrBatchOptimizerRuleMatch] = strconv.FormatBool(sel.Matched)
|
||||
groupBy := normalizeBatchOptimizationGrouping(sel.GroupBy)
|
||||
if groupBy == BatchOptimizationGroupingUnspecified {
|
||||
delete(intent.Attributes, attrBatchOptimizerGrouping)
|
||||
} else {
|
||||
intent.Attributes[attrBatchOptimizerGrouping] = string(groupBy)
|
||||
}
|
||||
ruleID := strings.TrimSpace(sel.RuleID)
|
||||
if ruleID == "" {
|
||||
delete(intent.Attributes, attrBatchOptimizerRuleID)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package psvc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/qsnap"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
func TestBatchOptimizationContextFromItem_UsesOperationTargetContext(t *testing.T) {
|
||||
item := qsnap.ResolvedItem{
|
||||
IntentRef: "intent-1",
|
||||
IntentSnapshot: model.PaymentIntent{
|
||||
Ref: "intent-1",
|
||||
Destination: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeCard,
|
||||
Card: &model.CardEndpoint{
|
||||
Pan: "2200001100000001",
|
||||
ExpMonth: 1,
|
||||
ExpYear: 2030,
|
||||
},
|
||||
},
|
||||
Amount: &paymenttypes.Money{Amount: "100", Currency: "RUB"},
|
||||
SettlementCurrency: "RUB",
|
||||
},
|
||||
QuoteSnapshot: &model.PaymentQuoteSnapshot{
|
||||
Route: &paymenttypes.QuoteRouteSpecification{
|
||||
Hops: []*paymenttypes.QuoteRouteHop{
|
||||
{Index: 1, Rail: "CRYPTO", Gateway: "gw-crypto", Network: "TRON_NILE", Role: paymenttypes.QuoteRouteHopRoleSource},
|
||||
{Index: 2, Rail: "CARD", Gateway: "mcards", Network: "TRON_NILE", Role: paymenttypes.QuoteRouteHopRoleDestination},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := batchOptimizationContextFromItem(item)
|
||||
if got, want := ctx.Rail, model.ParseRail(string(discovery.RailCardPayout)); got != want {
|
||||
t.Fatalf("operation rail mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := ctx.Provider, "mcards"; got != want {
|
||||
t.Fatalf("provider mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := ctx.Network, "TRON_NILE"; got != want {
|
||||
t.Fatalf("network mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
@@ -201,7 +202,7 @@ func inheritedExternalRefs(payment *agg.Payment, step xplan.Step, current agg.St
|
||||
}
|
||||
|
||||
func inheritedExecutedMoney(payment *agg.Payment, step xplan.Step, current agg.StepExecution) *paymenttypes.Money {
|
||||
if money := cloneStepMoney(current.ExecutedMoney); money != nil {
|
||||
if money := svcshared.CloneMoneyTrimNonEmpty(current.ExecutedMoney); money != nil {
|
||||
return money
|
||||
}
|
||||
if payment == nil || len(step.DependsOn) == 0 {
|
||||
@@ -213,7 +214,7 @@ func inheritedExecutedMoney(payment *agg.Payment, step xplan.Step, current agg.S
|
||||
if !ok || idx < 0 || idx >= len(payment.StepExecutions) {
|
||||
continue
|
||||
}
|
||||
if money := cloneStepMoney(payment.StepExecutions[idx].ExecutedMoney); money != nil {
|
||||
if money := svcshared.CloneMoneyTrimNonEmpty(payment.StepExecutions[idx].ExecutedMoney); money != nil {
|
||||
return money
|
||||
}
|
||||
}
|
||||
@@ -221,7 +222,7 @@ func inheritedExecutedMoney(payment *agg.Payment, step xplan.Step, current agg.S
|
||||
}
|
||||
|
||||
func inheritedConvertedMoney(payment *agg.Payment, step xplan.Step, current agg.StepExecution) *paymenttypes.Money {
|
||||
if money := cloneStepMoney(current.ConvertedMoney); money != nil {
|
||||
if money := svcshared.CloneMoneyTrimNonEmpty(current.ConvertedMoney); money != nil {
|
||||
return money
|
||||
}
|
||||
if payment == nil || len(step.DependsOn) == 0 {
|
||||
@@ -233,7 +234,7 @@ func inheritedConvertedMoney(payment *agg.Payment, step xplan.Step, current agg.
|
||||
if !ok || idx < 0 || idx >= len(payment.StepExecutions) {
|
||||
continue
|
||||
}
|
||||
if money := cloneStepMoney(payment.StepExecutions[idx].ConvertedMoney); money != nil {
|
||||
if money := svcshared.CloneMoneyTrimNonEmpty(payment.StepExecutions[idx].ConvertedMoney); money != nil {
|
||||
return money
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/qsnap"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/reqval"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
"go.uber.org/zap"
|
||||
@@ -168,7 +169,7 @@ func (s *svc) createNewPayment(ctx context.Context, requestCtx *reqval.Ctx) (*ag
|
||||
ClientPaymentRef: requestCtx.ClientPaymentRef,
|
||||
IntentSnapshot: resolved.IntentSnapshot,
|
||||
QuoteSnapshot: resolved.QuoteSnapshot,
|
||||
Steps: toStepShells(graph),
|
||||
Steps: toStepShells(graph, resolved.IntentSnapshot, resolved.QuoteSnapshot),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -212,20 +213,23 @@ func (s *svc) resolveAndPlan(ctx context.Context, requestCtx *reqval.Ctx) (*qsna
|
||||
return resolved, graph, nil
|
||||
}
|
||||
|
||||
func toStepShells(graph *xplan.Graph) []agg.StepShell {
|
||||
func toStepShells(graph *xplan.Graph, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) []agg.StepShell {
|
||||
if graph == nil || len(graph.Steps) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]agg.StepShell, 0, len(graph.Steps))
|
||||
for i := range graph.Steps {
|
||||
plannedMoney, plannedConverted := plannedMoneyForStep(graph.Steps[i], intent, quote)
|
||||
out = append(out, agg.StepShell{
|
||||
StepRef: graph.Steps[i].StepRef,
|
||||
StepCode: graph.Steps[i].StepCode,
|
||||
Rail: graph.Steps[i].Rail,
|
||||
Gateway: graph.Steps[i].Gateway,
|
||||
InstanceID: graph.Steps[i].InstanceID,
|
||||
ReportVisibility: graph.Steps[i].Visibility,
|
||||
UserLabel: graph.Steps[i].UserLabel,
|
||||
StepRef: graph.Steps[i].StepRef,
|
||||
StepCode: graph.Steps[i].StepCode,
|
||||
Rail: graph.Steps[i].Rail,
|
||||
Gateway: graph.Steps[i].Gateway,
|
||||
InstanceID: graph.Steps[i].InstanceID,
|
||||
ReportVisibility: graph.Steps[i].Visibility,
|
||||
UserLabel: graph.Steps[i].UserLabel,
|
||||
PlannedMoney: plannedMoney,
|
||||
PlannedConvertedMoney: plannedConverted,
|
||||
})
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
attrAggregatedByRecipient = "orchestrator.v2.aggregated_by_recipient"
|
||||
attrAggregatedItems = "orchestrator.v2.aggregated_items"
|
||||
attrBatchOptimizerMode = "orchestrator.v2.batch_optimizer_mode"
|
||||
attrBatchOptimizerGrouping = "orchestrator.v2.batch_optimizer_group_by"
|
||||
attrBatchOptimizerRuleID = "orchestrator.v2.batch_optimizer_rule_id"
|
||||
attrBatchOptimizerRuleMatch = "orchestrator.v2.batch_optimizer_rule_matched"
|
||||
)
|
||||
@@ -198,7 +199,7 @@ func (s *svc) createGroupPayment(
|
||||
ClientPaymentRef: requestCtx.ClientPaymentRef,
|
||||
IntentSnapshot: intentSnapshot,
|
||||
QuoteSnapshot: group.QuoteSnapshot,
|
||||
Steps: toStepShells(graph),
|
||||
Steps: toStepShells(graph, intentSnapshot, group.QuoteSnapshot),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -291,7 +292,7 @@ func (s *svc) buildBatchOperationGroups(groups []opagg.Group) ([]opagg.Group, er
|
||||
}
|
||||
group.IntentSnapshot.Attributes[attrAggregatedItems] = strconv.Itoa(len(group.IntentRefs))
|
||||
|
||||
targets := buildBatchPayoutTargets([]opagg.Group{group})
|
||||
targets := buildBatchPayoutTargets(group)
|
||||
if routeContainsCardPayout(group.QuoteSnapshot) && len(targets) > 0 {
|
||||
raw, err := batchmeta.EncodePayoutTargets(targets)
|
||||
if err != nil {
|
||||
@@ -311,32 +312,49 @@ func (s *svc) buildBatchOperationGroups(groups []opagg.Group) ([]opagg.Group, er
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildBatchPayoutTargets(groups []opagg.Group) []batchmeta.PayoutTarget {
|
||||
if len(groups) == 0 {
|
||||
func buildBatchPayoutTargets(group opagg.Group) []batchmeta.PayoutTarget {
|
||||
if len(group.Members) > 0 {
|
||||
targets := make([]batchmeta.PayoutTarget, 0, len(group.Members))
|
||||
for i := range group.Members {
|
||||
member := group.Members[i]
|
||||
target := batchmeta.PayoutTarget{
|
||||
TargetRef: firstNonEmpty(strings.TrimSpace(member.IntentRef), "recipient-"+strconv.Itoa(i+1)),
|
||||
IntentRef: normalizeIntentRefs([]string{member.IntentRef}),
|
||||
Amount: batchPayoutAmountFromMember(member),
|
||||
}
|
||||
if member.IntentSnapshot.Destination.Type == model.EndpointTypeCard && member.IntentSnapshot.Destination.Card != nil {
|
||||
card := *member.IntentSnapshot.Destination.Card
|
||||
target.Card = &card
|
||||
}
|
||||
if member.IntentSnapshot.Customer != nil {
|
||||
customer := *member.IntentSnapshot.Customer
|
||||
target.Customer = &customer
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
if isEmptyIntentSnapshot(group.IntentSnapshot) {
|
||||
return nil
|
||||
}
|
||||
out := make([]batchmeta.PayoutTarget, 0, len(groups))
|
||||
for i := range groups {
|
||||
group := groups[i]
|
||||
target := batchmeta.PayoutTarget{
|
||||
TargetRef: firstNonEmpty(strings.TrimSpace(group.RecipientKey), "recipient-"+strconv.Itoa(i+1)),
|
||||
IntentRef: normalizeIntentRefs(group.IntentRefs),
|
||||
Amount: batchPayoutAmount(group),
|
||||
}
|
||||
if group.IntentSnapshot.Destination.Type == model.EndpointTypeCard && group.IntentSnapshot.Destination.Card != nil {
|
||||
card := *group.IntentSnapshot.Destination.Card
|
||||
target.Card = &card
|
||||
}
|
||||
if group.IntentSnapshot.Customer != nil {
|
||||
customer := *group.IntentSnapshot.Customer
|
||||
target.Customer = &customer
|
||||
}
|
||||
out = append(out, target)
|
||||
target := batchmeta.PayoutTarget{
|
||||
TargetRef: firstNonEmpty(strings.TrimSpace(group.RecipientKey), "recipient-1"),
|
||||
IntentRef: normalizeIntentRefs(group.IntentRefs),
|
||||
Amount: batchPayoutAmount(group),
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
if group.IntentSnapshot.Destination.Type == model.EndpointTypeCard && group.IntentSnapshot.Destination.Card != nil {
|
||||
card := *group.IntentSnapshot.Destination.Card
|
||||
target.Card = &card
|
||||
}
|
||||
return out
|
||||
if group.IntentSnapshot.Customer != nil {
|
||||
customer := *group.IntentSnapshot.Customer
|
||||
target.Customer = &customer
|
||||
}
|
||||
return []batchmeta.PayoutTarget{target}
|
||||
}
|
||||
|
||||
func batchPayoutAmount(group opagg.Group) *paymenttypes.Money {
|
||||
@@ -355,6 +373,26 @@ func batchPayoutAmount(group opagg.Group) *paymenttypes.Money {
|
||||
}
|
||||
}
|
||||
|
||||
func batchPayoutAmountFromMember(member opagg.GroupMember) *paymenttypes.Money {
|
||||
if member.QuoteSnapshot != nil && member.QuoteSnapshot.ExpectedSettlementAmount != nil {
|
||||
return &paymenttypes.Money{
|
||||
Amount: strings.TrimSpace(member.QuoteSnapshot.ExpectedSettlementAmount.Amount),
|
||||
Currency: strings.TrimSpace(member.QuoteSnapshot.ExpectedSettlementAmount.Currency),
|
||||
}
|
||||
}
|
||||
if member.IntentSnapshot.Amount == nil {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: strings.TrimSpace(member.IntentSnapshot.Amount.Amount),
|
||||
Currency: strings.TrimSpace(member.IntentSnapshot.Amount.Currency),
|
||||
}
|
||||
}
|
||||
|
||||
func isEmptyIntentSnapshot(intent model.PaymentIntent) bool {
|
||||
return intent.Amount == nil && (intent.Kind == "" || intent.Kind == model.PaymentKindUnspecified)
|
||||
}
|
||||
|
||||
func routeContainsCardPayout(snapshot *model.PaymentQuoteSnapshot) bool {
|
||||
if snapshot == nil || snapshot.Route == nil {
|
||||
return false
|
||||
|
||||
@@ -162,6 +162,47 @@ func TestExecuteBatchPayment_CryptoPolicyMergesByDestination(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteBatchPayment_CryptoPolicyMergesSameRecipientEvenWithDifferentSourceAndQuoteRefs(t *testing.T) {
|
||||
env := newTestEnvWithPolicy(t, BatchOptimizationPolicy{
|
||||
DefaultMode: BatchOptimizationModeNoOptimization,
|
||||
Rules: []BatchOptimizationRule{
|
||||
{
|
||||
ID: "crypto-merge",
|
||||
Priority: 100,
|
||||
Mode: BatchOptimizationModeMergeByDestination,
|
||||
Match: BatchOptimizationMatch{
|
||||
Rail: discovery.RailCrypto,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
|
||||
step := req.StepExecution
|
||||
step.State = agg.StepStateCompleted
|
||||
return &sexec.ExecuteOutput{StepExecution: step}, nil
|
||||
})
|
||||
|
||||
quote := newExecutableBatchCryptoQuoteSameDestination(env.orgID, "quote-batch-crypto-merge-mixed")
|
||||
if len(quote.Items) != 2 {
|
||||
t.Fatalf("expected 2 quote items, got=%d", len(quote.Items))
|
||||
}
|
||||
quote.Items[0].Quote.Route.RouteRef = "route-a"
|
||||
quote.Items[1].Intent.Source = testLedgerEndpoint("ledger-src-2")
|
||||
quote.Items[1].Quote.Route.RouteRef = "route-b"
|
||||
env.quotes.Put(quote)
|
||||
|
||||
resp, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{
|
||||
Meta: testMeta(env.orgID, "idem-batch-crypto-merge-mixed"),
|
||||
QuotationRef: "quote-batch-crypto-merge-mixed",
|
||||
ClientPaymentRef: "client-batch-crypto-merge-mixed",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteBatchPayment returned error: %v", err)
|
||||
}
|
||||
if got, want := len(resp.GetPayments()), 1; got != want {
|
||||
t.Fatalf("expected %d merged payment for crypto policy, got=%d", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteBatchPayment_IdempotentRetry(t *testing.T) {
|
||||
env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
|
||||
step := req.StepExecution
|
||||
|
||||
@@ -17,11 +17,21 @@ const (
|
||||
BatchOptimizationModeNoOptimization BatchOptimizationMode = "no_optimization"
|
||||
)
|
||||
|
||||
// BatchOptimizationGrouping defines which target key is used for merge grouping.
|
||||
type BatchOptimizationGrouping string
|
||||
|
||||
const (
|
||||
BatchOptimizationGroupingUnspecified BatchOptimizationGrouping = ""
|
||||
BatchOptimizationGroupingOperationDestination BatchOptimizationGrouping = "operation_destination"
|
||||
BatchOptimizationGroupingOperationSource BatchOptimizationGrouping = "operation_source"
|
||||
BatchOptimizationGroupingRailTarget BatchOptimizationGrouping = "rail_target"
|
||||
)
|
||||
|
||||
// BatchOptimizationPolicy configures batch operation compaction behavior.
|
||||
//
|
||||
// Rules are evaluated by "best match":
|
||||
// 1. all specified match fields must match the item context;
|
||||
// 2. winner is highest specificity (number of optional selectors present);
|
||||
// 2. winner is highest specificity (number of optional match fields present);
|
||||
// 3. ties are resolved by larger Priority;
|
||||
// 4. remaining ties keep the first rule order from config.
|
||||
//
|
||||
@@ -37,6 +47,7 @@ type BatchOptimizationRule struct {
|
||||
Enabled *bool
|
||||
Priority int
|
||||
Mode BatchOptimizationMode
|
||||
GroupBy BatchOptimizationGrouping
|
||||
Match BatchOptimizationMatch
|
||||
}
|
||||
|
||||
@@ -67,9 +78,11 @@ type BatchOptimizationContext struct {
|
||||
|
||||
// BatchOptimizationSelection is the selected optimization decision.
|
||||
type BatchOptimizationSelection struct {
|
||||
Mode BatchOptimizationMode
|
||||
RuleID string
|
||||
Matched bool
|
||||
Mode BatchOptimizationMode
|
||||
RuleID string
|
||||
Matched bool
|
||||
GroupBy BatchOptimizationGrouping
|
||||
MatchRail model.Rail
|
||||
}
|
||||
|
||||
func defaultBatchOptimizationPolicy() BatchOptimizationPolicy {
|
||||
@@ -107,10 +120,14 @@ func normalizeBatchOptimizationRule(in BatchOptimizationRule) (BatchOptimization
|
||||
Enabled: in.Enabled,
|
||||
Priority: in.Priority,
|
||||
Mode: normalizeBatchOptimizationMode(in.Mode),
|
||||
GroupBy: normalizeBatchOptimizationGrouping(in.GroupBy),
|
||||
}
|
||||
if out.Mode == BatchOptimizationModeUnspecified {
|
||||
return BatchOptimizationRule{}, false
|
||||
}
|
||||
if out.GroupBy == BatchOptimizationGroupingUnspecified {
|
||||
out.GroupBy = BatchOptimizationGroupingOperationDestination
|
||||
}
|
||||
|
||||
match, ok := normalizeBatchOptimizationMatch(in.Match)
|
||||
if !ok {
|
||||
@@ -174,41 +191,78 @@ func normalizeBatchOptimizationAmountRange(in BatchOptimizationAmountRange) (Bat
|
||||
}
|
||||
|
||||
func (p BatchOptimizationPolicy) Select(in BatchOptimizationContext) BatchOptimizationSelection {
|
||||
return p.SelectMany([]BatchOptimizationContext{in})
|
||||
}
|
||||
|
||||
func (p BatchOptimizationPolicy) SelectMany(inputs []BatchOptimizationContext) BatchOptimizationSelection {
|
||||
policy := normalizeBatchOptimizationPolicy(p)
|
||||
ctx := normalizeBatchOptimizationContext(in)
|
||||
contexts := make([]BatchOptimizationContext, 0, len(inputs))
|
||||
for i := range inputs {
|
||||
ctx := normalizeBatchOptimizationContext(inputs[i])
|
||||
if ctx.Rail == discovery.RailUnspecified {
|
||||
continue
|
||||
}
|
||||
contexts = append(contexts, ctx)
|
||||
}
|
||||
|
||||
selected := BatchOptimizationSelection{
|
||||
Mode: policy.DefaultMode,
|
||||
Mode: policy.DefaultMode,
|
||||
GroupBy: BatchOptimizationGroupingOperationDestination,
|
||||
MatchRail: discovery.RailUnspecified,
|
||||
}
|
||||
if len(contexts) == 0 {
|
||||
return selected
|
||||
}
|
||||
|
||||
bestSpecificity := -1
|
||||
bestPriority := 0
|
||||
bestOrder := len(policy.Rules) + 1
|
||||
for i := range policy.Rules {
|
||||
rule := policy.Rules[i]
|
||||
if rule.Enabled != nil && !*rule.Enabled {
|
||||
continue
|
||||
}
|
||||
specificity, ok := rule.matches(ctx)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if specificity > bestSpecificity ||
|
||||
(specificity == bestSpecificity && rule.Priority > bestPriority) ||
|
||||
(specificity == bestSpecificity && rule.Priority == bestPriority && i < bestOrder) {
|
||||
selected = BatchOptimizationSelection{
|
||||
Mode: rule.Mode,
|
||||
RuleID: rule.ID,
|
||||
Matched: true,
|
||||
bestContext := len(contexts) + 1
|
||||
for cIdx := range contexts {
|
||||
ctx := contexts[cIdx]
|
||||
for rIdx := range policy.Rules {
|
||||
rule := policy.Rules[rIdx]
|
||||
if rule.Enabled != nil && !*rule.Enabled {
|
||||
continue
|
||||
}
|
||||
specificity, ok := rule.matches(ctx)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if specificity > bestSpecificity ||
|
||||
(specificity == bestSpecificity && rule.Priority > bestPriority) ||
|
||||
(specificity == bestSpecificity && rule.Priority == bestPriority && rIdx < bestOrder) ||
|
||||
(specificity == bestSpecificity && rule.Priority == bestPriority && rIdx == bestOrder && cIdx < bestContext) {
|
||||
selected = BatchOptimizationSelection{
|
||||
Mode: rule.Mode,
|
||||
RuleID: rule.ID,
|
||||
Matched: true,
|
||||
GroupBy: rule.GroupBy,
|
||||
MatchRail: rule.Match.Rail,
|
||||
}
|
||||
bestSpecificity = specificity
|
||||
bestPriority = rule.Priority
|
||||
bestOrder = rIdx
|
||||
bestContext = cIdx
|
||||
}
|
||||
bestSpecificity = specificity
|
||||
bestPriority = rule.Priority
|
||||
bestOrder = i
|
||||
}
|
||||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
func normalizeBatchOptimizationGrouping(raw BatchOptimizationGrouping) BatchOptimizationGrouping {
|
||||
switch strings.ToLower(strings.TrimSpace(string(raw))) {
|
||||
case "operation_destination", "operation-destination", "destination", "intent_destination", "intent-destination":
|
||||
return BatchOptimizationGroupingOperationDestination
|
||||
case "operation_source", "operation-source", "source", "intent_source", "intent-source":
|
||||
return BatchOptimizationGroupingOperationSource
|
||||
case "rail_target", "rail-target", "by_rail_target", "by-rail-target":
|
||||
return BatchOptimizationGroupingRailTarget
|
||||
default:
|
||||
return BatchOptimizationGroupingUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func (r BatchOptimizationRule) matches(ctx BatchOptimizationContext) (int, bool) {
|
||||
match := r.Match
|
||||
if match.Rail == discovery.RailUnspecified || ctx.Rail == discovery.RailUnspecified {
|
||||
|
||||
@@ -53,6 +53,9 @@ func TestNormalizeBatchOptimizationPolicy_DropsInvalidRules(t *testing.T) {
|
||||
if got.Rules[0].ID != "valid" {
|
||||
t.Fatalf("rule id mismatch: got=%q want=%q", got.Rules[0].ID, "valid")
|
||||
}
|
||||
if got, want := got.Rules[0].GroupBy, BatchOptimizationGroupingOperationDestination; got != want {
|
||||
t.Fatalf("group_by mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchOptimizationPolicy_Select_UsesBestSpecificityThenPriority(t *testing.T) {
|
||||
@@ -101,6 +104,12 @@ func TestBatchOptimizationPolicy_Select_UsesBestSpecificityThenPriority(t *testi
|
||||
if out.Mode != BatchOptimizationModeMergeByDestination {
|
||||
t.Fatalf("mode mismatch: got=%q want=%q", out.Mode, BatchOptimizationModeMergeByDestination)
|
||||
}
|
||||
if got, want := out.GroupBy, BatchOptimizationGroupingOperationDestination; got != want {
|
||||
t.Fatalf("group_by mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := string(out.MatchRail), string(discovery.RailCrypto); got != want {
|
||||
t.Fatalf("match rail mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchOptimizationPolicy_Select_FallsBackToDefault(t *testing.T) {
|
||||
@@ -124,6 +133,9 @@ func TestBatchOptimizationPolicy_Select_FallsBackToDefault(t *testing.T) {
|
||||
if out.Mode != BatchOptimizationModeNoOptimization {
|
||||
t.Fatalf("mode mismatch: got=%q want=%q", out.Mode, BatchOptimizationModeNoOptimization)
|
||||
}
|
||||
if got, want := out.GroupBy, BatchOptimizationGroupingOperationDestination; got != want {
|
||||
t.Fatalf("default group_by mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchOptimizationPolicy_Select_MatchesAmountRange(t *testing.T) {
|
||||
@@ -165,3 +177,26 @@ func TestBatchOptimizationPolicy_Select_MatchesAmountRange(t *testing.T) {
|
||||
t.Fatalf("default mode mismatch: got=%q want=%q", notMatched.Mode, BatchOptimizationModeNoOptimization)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchOptimizationPolicy_Select_ReturnsConfiguredGroupBy(t *testing.T) {
|
||||
policy := BatchOptimizationPolicy{
|
||||
DefaultMode: BatchOptimizationModeNoOptimization,
|
||||
Rules: []BatchOptimizationRule{
|
||||
{
|
||||
ID: "crypto-by-rail-target",
|
||||
Mode: BatchOptimizationModeMergeByDestination,
|
||||
GroupBy: BatchOptimizationGroupingRailTarget,
|
||||
Match: BatchOptimizationMatch{
|
||||
Rail: discovery.RailCrypto,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
out := policy.Select(BatchOptimizationContext{Rail: discovery.RailCrypto})
|
||||
if !out.Matched {
|
||||
t.Fatal("expected matched rule")
|
||||
}
|
||||
if got, want := out.GroupBy, BatchOptimizationGroupingRailTarget; got != want {
|
||||
t.Fatalf("group_by mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package psvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
||||
)
|
||||
|
||||
func plannedMoneyForStep(
|
||||
step xplan.Step,
|
||||
intent model.PaymentIntent,
|
||||
quote *model.PaymentQuoteSnapshot,
|
||||
) (*paymenttypes.Money, *paymenttypes.Money) {
|
||||
action := model.ParseRailOperation(string(step.Action))
|
||||
|
||||
switch step.Rail {
|
||||
case discovery.RailLedger:
|
||||
return plannedLedgerMoney(step, intent, quote, action), nil
|
||||
case discovery.RailCrypto:
|
||||
switch action {
|
||||
case discovery.RailOperationSend:
|
||||
return plannedSourceMoney(intent, quote), nil
|
||||
case discovery.RailOperationFee:
|
||||
return plannedFeeMoney(intent, quote), nil
|
||||
}
|
||||
case discovery.RailProviderSettlement:
|
||||
if action == discovery.RailOperationFXConvert {
|
||||
return plannedSourceMoney(intent, quote), plannedSettlementMoney(intent, quote)
|
||||
}
|
||||
case discovery.RailCardPayout:
|
||||
if action == discovery.RailOperationSend {
|
||||
return plannedCardPayoutMoney(step, intent, quote), nil
|
||||
}
|
||||
}
|
||||
|
||||
if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok {
|
||||
return svcshared.CloneMoneyTrimNonEmpty(override), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func plannedLedgerMoney(
|
||||
step xplan.Step,
|
||||
intent model.PaymentIntent,
|
||||
quote *model.PaymentQuoteSnapshot,
|
||||
action model.RailOperation,
|
||||
) *paymenttypes.Money {
|
||||
if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok {
|
||||
return svcshared.CloneMoneyTrimNonEmpty(override)
|
||||
}
|
||||
|
||||
sourceMoney := plannedSourceMoney(intent, quote)
|
||||
settlementMoney := plannedSettlementMoney(intent, quote)
|
||||
payoutMoney := settlementMoney
|
||||
|
||||
if fromRail, toRail, ok := plannedLedgerBoundaryRails(step.StepCode, quote); ok {
|
||||
switch {
|
||||
case isLedgerExternalRail(fromRail) && isLedgerExternalRail(toRail):
|
||||
return sourceMoney
|
||||
case isLedgerExternalRail(fromRail) && isLedgerInternalRail(toRail):
|
||||
return settlementMoney
|
||||
case isLedgerInternalRail(fromRail) && isLedgerExternalRail(toRail):
|
||||
if toRail == discovery.RailCardPayout {
|
||||
return payoutMoney
|
||||
}
|
||||
return settlementMoney
|
||||
case isLedgerInternalRail(fromRail) && isLedgerInternalRail(toRail):
|
||||
return settlementMoney
|
||||
}
|
||||
}
|
||||
|
||||
switch action {
|
||||
case discovery.RailOperationCredit, discovery.RailOperationExternalCredit:
|
||||
return settlementMoney
|
||||
case discovery.RailOperationDebit, discovery.RailOperationExternalDebit:
|
||||
if sourceMoney != nil {
|
||||
return sourceMoney
|
||||
}
|
||||
return settlementMoney
|
||||
case discovery.RailOperationMove, discovery.RailOperationBlock, discovery.RailOperationRelease:
|
||||
if settlementMoney != nil {
|
||||
return settlementMoney
|
||||
}
|
||||
return sourceMoney
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func plannedSourceMoney(intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money {
|
||||
if quote != nil && quote.DebitAmount != nil {
|
||||
return svcshared.CloneMoneyTrimNonEmpty(quote.DebitAmount)
|
||||
}
|
||||
return svcshared.CloneMoneyTrimNonEmpty(intent.Amount)
|
||||
}
|
||||
|
||||
func plannedSettlementMoney(intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money {
|
||||
if quote != nil && quote.ExpectedSettlementAmount != nil {
|
||||
return svcshared.CloneMoneyTrimNonEmpty(quote.ExpectedSettlementAmount)
|
||||
}
|
||||
return plannedSourceMoney(intent, quote)
|
||||
}
|
||||
|
||||
func plannedCardPayoutMoney(step xplan.Step, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money {
|
||||
if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok {
|
||||
return svcshared.CloneMoneyTrimNonEmpty(override)
|
||||
}
|
||||
if quote != nil && quote.ExpectedSettlementAmount != nil {
|
||||
return svcshared.CloneMoneyTrimNonEmpty(quote.ExpectedSettlementAmount)
|
||||
}
|
||||
return svcshared.CloneMoneyTrimNonEmpty(intent.Amount)
|
||||
}
|
||||
|
||||
func plannedFeeMoney(intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money {
|
||||
if quote == nil || len(quote.FeeLines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourceCurrency := ""
|
||||
if source := plannedSourceMoney(intent, quote); source != nil {
|
||||
sourceCurrency = strings.TrimSpace(source.Currency)
|
||||
}
|
||||
|
||||
total := decimal.Zero
|
||||
currency := ""
|
||||
for i := range quote.FeeLines {
|
||||
line := quote.FeeLines[i]
|
||||
if line == nil || line.GetMoney() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lineCurrency := strings.TrimSpace(line.GetMoney().GetCurrency())
|
||||
if lineCurrency == "" {
|
||||
continue
|
||||
}
|
||||
if sourceCurrency != "" && !strings.EqualFold(sourceCurrency, lineCurrency) {
|
||||
continue
|
||||
}
|
||||
if currency == "" {
|
||||
currency = strings.ToUpper(lineCurrency)
|
||||
} else if !strings.EqualFold(currency, lineCurrency) {
|
||||
return nil
|
||||
}
|
||||
|
||||
amountRaw := strings.TrimSpace(line.GetMoney().GetAmount())
|
||||
amount, err := decimal.NewFromString(amountRaw)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
amount = amount.Abs()
|
||||
if amount.IsZero() {
|
||||
continue
|
||||
}
|
||||
if line.GetSide() == paymenttypes.EntrySideCredit {
|
||||
total = total.Sub(amount)
|
||||
} else {
|
||||
total = total.Add(amount)
|
||||
}
|
||||
}
|
||||
|
||||
if total.Sign() <= 0 || strings.TrimSpace(currency) == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: total.String(),
|
||||
Currency: strings.ToUpper(strings.TrimSpace(currency)),
|
||||
}
|
||||
}
|
||||
|
||||
func plannedLedgerBoundaryRails(stepCode string, quote *model.PaymentQuoteSnapshot) (model.Rail, model.Rail, bool) {
|
||||
fromIndex, toIndex, ok := parseLedgerEdgeStepCode(stepCode)
|
||||
if !ok || quote == nil || quote.Route == nil {
|
||||
return discovery.RailUnspecified, discovery.RailUnspecified, false
|
||||
}
|
||||
|
||||
var fromRail model.Rail = discovery.RailUnspecified
|
||||
var toRail model.Rail = discovery.RailUnspecified
|
||||
for i := range quote.Route.Hops {
|
||||
hop := quote.Route.Hops[i]
|
||||
if hop == nil {
|
||||
continue
|
||||
}
|
||||
if hop.Index == fromIndex {
|
||||
fromRail = model.ParseRail(hop.Rail)
|
||||
}
|
||||
if hop.Index == toIndex {
|
||||
toRail = model.ParseRail(hop.Rail)
|
||||
}
|
||||
}
|
||||
if fromRail == discovery.RailUnspecified || toRail == discovery.RailUnspecified {
|
||||
return discovery.RailUnspecified, discovery.RailUnspecified, false
|
||||
}
|
||||
return fromRail, toRail, true
|
||||
}
|
||||
|
||||
func parseLedgerEdgeStepCode(stepCode string) (uint32, uint32, bool) {
|
||||
code := strings.ToLower(strings.TrimSpace(stepCode))
|
||||
if !strings.HasPrefix(code, "edge.") || !strings.Contains(code, ".ledger.") {
|
||||
return 0, 0, false
|
||||
}
|
||||
var (
|
||||
from uint32
|
||||
to uint32
|
||||
op string
|
||||
)
|
||||
if _, err := fmt.Sscanf(code, "edge.%d_%d.ledger.%s", &from, &to, &op); err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
if strings.TrimSpace(op) == "" {
|
||||
return 0, 0, false
|
||||
}
|
||||
return from, to, true
|
||||
}
|
||||
|
||||
func isLedgerInternalRail(rail model.Rail) bool {
|
||||
return rail == discovery.RailLedger
|
||||
}
|
||||
|
||||
func isLedgerExternalRail(rail model.Rail) bool {
|
||||
switch rail {
|
||||
case discovery.RailCrypto, discovery.RailProviderSettlement, discovery.RailCardPayout, discovery.RailFiatOnRamp:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ssched"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -325,8 +326,10 @@ func normalizeExecutorOutput(current agg.StepExecution, out *sexec.ExecuteOutput
|
||||
next.Attempt = out.StepExecution.Attempt
|
||||
}
|
||||
next.ExternalRefs = out.StepExecution.ExternalRefs
|
||||
next.ExecutedMoney = cloneStepMoney(out.StepExecution.ExecutedMoney)
|
||||
next.ConvertedMoney = cloneStepMoney(out.StepExecution.ConvertedMoney)
|
||||
next.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.ExecutedMoney)
|
||||
next.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.ConvertedMoney)
|
||||
next.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.PlannedMoney)
|
||||
next.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.PlannedConvertedMoney)
|
||||
next.FailureCode = strings.TrimSpace(out.StepExecution.FailureCode)
|
||||
next.FailureMsg = strings.TrimSpace(out.StepExecution.FailureMsg)
|
||||
|
||||
@@ -445,6 +448,12 @@ func stepExecutionEqual(left, right agg.StepExecution) bool {
|
||||
if !stepMoneyEqual(left.ConvertedMoney, right.ConvertedMoney) {
|
||||
return false
|
||||
}
|
||||
if !stepMoneyEqual(left.PlannedMoney, right.PlannedMoney) {
|
||||
return false
|
||||
}
|
||||
if !stepMoneyEqual(left.PlannedConvertedMoney, right.PlannedConvertedMoney) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
package psvc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
func cloneStepMoney(src *paymenttypes.Money) *paymenttypes.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(src.GetAmount())
|
||||
currency := strings.TrimSpace(src.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func stepMoneyEqual(left, right *paymenttypes.Money) bool {
|
||||
left = cloneStepMoney(left)
|
||||
right = cloneStepMoney(right)
|
||||
left = svcshared.CloneMoneyTrimNonEmpty(left)
|
||||
right = svcshared.CloneMoneyTrimNonEmpty(right)
|
||||
switch {
|
||||
case left == nil && right == nil:
|
||||
return true
|
||||
|
||||
@@ -207,6 +207,19 @@ func TestExecute_UnsupportedProviderSettlementSend(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_UnsupportedLedgerFee(t *testing.T) {
|
||||
registry := New(Dependencies{})
|
||||
|
||||
_, err := registry.Execute(context.Background(), ExecuteInput{
|
||||
Payment: &agg.Payment{PaymentRef: "p1"},
|
||||
Step: xplan.Step{StepRef: "s1", StepCode: "ledger.fee", Action: discovery.RailOperationFee, Rail: discovery.RailLedger},
|
||||
StepExecution: agg.StepExecution{StepRef: "s1", StepCode: "ledger.fee", Attempt: 1},
|
||||
})
|
||||
if !errors.Is(err, ErrUnsupportedStep) {
|
||||
t.Fatalf("expected ErrUnsupportedStep, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_MissingExecutor(t *testing.T) {
|
||||
registry := New(Dependencies{})
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oshared"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
func (s *svc) prepareInput(in Input) (*preparedInput, error) {
|
||||
@@ -174,9 +175,11 @@ func (s *svc) normalizeStepExecution(exec agg.StepExecution, index int) (agg.Ste
|
||||
exec.InstanceID = strings.TrimSpace(exec.InstanceID)
|
||||
exec.Rail = model.ParseRail(string(exec.Rail))
|
||||
exec.ReportVisibility = model.NormalizeReportVisibility(exec.ReportVisibility)
|
||||
exec.ExternalRefs = cloneExternalRefs(exec.ExternalRefs)
|
||||
exec.ExecutedMoney = cloneStepMoney(exec.ExecutedMoney)
|
||||
exec.ConvertedMoney = cloneStepMoney(exec.ConvertedMoney)
|
||||
exec.ExternalRefs = oshared.CloneExternalRefs(exec.ExternalRefs)
|
||||
exec.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ExecutedMoney)
|
||||
exec.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ConvertedMoney)
|
||||
exec.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedMoney)
|
||||
exec.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedConvertedMoney)
|
||||
if exec.StepRef == "" {
|
||||
return agg.StepExecution{}, merrors.InvalidArgument("stepExecutions[" + itoa(index) + "].step_ref is required")
|
||||
}
|
||||
@@ -212,15 +215,17 @@ func seedMissingExecutions(
|
||||
maxAttemptsByRef[stepRef] = 1
|
||||
}
|
||||
executionsByRef[stepRef] = &agg.StepExecution{
|
||||
StepRef: step.StepRef,
|
||||
StepCode: step.StepCode,
|
||||
Rail: step.Rail,
|
||||
Gateway: strings.TrimSpace(step.Gateway),
|
||||
InstanceID: strings.TrimSpace(step.InstanceID),
|
||||
ReportVisibility: effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility),
|
||||
UserLabel: strings.TrimSpace(step.UserLabel),
|
||||
State: agg.StepStatePending,
|
||||
Attempt: attempt,
|
||||
StepRef: step.StepRef,
|
||||
StepCode: step.StepCode,
|
||||
Rail: step.Rail,
|
||||
Gateway: strings.TrimSpace(step.Gateway),
|
||||
InstanceID: strings.TrimSpace(step.InstanceID),
|
||||
ReportVisibility: effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility),
|
||||
UserLabel: strings.TrimSpace(step.UserLabel),
|
||||
State: agg.StepStatePending,
|
||||
Attempt: attempt,
|
||||
PlannedMoney: nil,
|
||||
PlannedConvertedMoney: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,39 +264,11 @@ func normalizeStepState(state agg.StepState) (agg.StepState, bool) {
|
||||
|
||||
func cloneStepExecution(exec agg.StepExecution) agg.StepExecution {
|
||||
out := exec
|
||||
out.ExternalRefs = cloneExternalRefs(exec.ExternalRefs)
|
||||
out.ExecutedMoney = cloneStepMoney(exec.ExecutedMoney)
|
||||
out.ConvertedMoney = cloneStepMoney(exec.ConvertedMoney)
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneStepMoney(money *paymenttypes.Money) *paymenttypes.Money {
|
||||
if money == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(money.GetAmount())
|
||||
currency := strings.TrimSpace(money.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneExternalRefs(refs []agg.ExternalRef) []agg.ExternalRef {
|
||||
if len(refs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]agg.ExternalRef, 0, len(refs))
|
||||
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)
|
||||
out = append(out, ref)
|
||||
}
|
||||
out.ExternalRefs = oshared.CloneExternalRefs(exec.ExternalRefs)
|
||||
out.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ExecutedMoney)
|
||||
out.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ConvertedMoney)
|
||||
out.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedMoney)
|
||||
out.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedConvertedMoney)
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ func TestCompile_ExternalToExternal_WithWalletFee_InsertsFeeStep(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompile_ExternalToExternal_IgnoresNonWalletFeeLines(t *testing.T) {
|
||||
func TestCompile_ExternalToExternal_IncludesNonWalletFeeLines(t *testing.T) {
|
||||
compiler := New()
|
||||
|
||||
graph, err := compiler.Compile(Input{
|
||||
@@ -195,14 +195,18 @@ func TestCompile_ExternalToExternal_IgnoresNonWalletFeeLines(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Compile returned error: %v", err)
|
||||
}
|
||||
if got, want := len(graph.Steps), 9; got != want {
|
||||
t.Fatalf("expected 9 steps, got %d", got)
|
||||
if got, want := len(graph.Steps), 10; got != want {
|
||||
t.Fatalf("expected 10 steps, got %d", got)
|
||||
}
|
||||
feeCount := 0
|
||||
for i := range graph.Steps {
|
||||
if got := graph.Steps[i].Action; got == discovery.RailOperationFee {
|
||||
t.Fatalf("unexpected fee step at index %d: %+v", i, graph.Steps[i])
|
||||
if graph.Steps[i].Action == discovery.RailOperationFee {
|
||||
feeCount++
|
||||
}
|
||||
}
|
||||
if got, want := feeCount, 1; got != want {
|
||||
t.Fatalf("fee steps mismatch: got=%d want=%d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompile_InternalToExternal_UsesHoldAndSettlementBranches(t *testing.T) {
|
||||
@@ -239,6 +243,51 @@ func TestCompile_InternalToExternal_UsesHoldAndSettlementBranches(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompile_InternalToExternal_WithWalletFee_InsertsFeeStep(t *testing.T) {
|
||||
compiler := New()
|
||||
|
||||
graph, err := compiler.Compile(Input{
|
||||
IntentSnapshot: testIntent(model.PaymentKindPayout),
|
||||
QuoteSnapshot: &model.PaymentQuoteSnapshot{
|
||||
DebitAmount: &paymenttypes.Money{Amount: "100", Currency: "USDT"},
|
||||
FeeLines: []*paymenttypes.FeeLine{
|
||||
{
|
||||
Money: &paymenttypes.Money{Amount: "1", Currency: "USDT"},
|
||||
Side: paymenttypes.EntrySideDebit,
|
||||
Meta: map[string]string{"fee_target": "wallet"},
|
||||
},
|
||||
},
|
||||
Route: &paymenttypes.QuoteRouteSpecification{
|
||||
Hops: []*paymenttypes.QuoteRouteHop{
|
||||
{Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource},
|
||||
{Index: 20, Rail: "CARD", Gateway: "gw-card", Role: paymenttypes.QuoteRouteHopRoleDestination},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Compile returned error: %v", err)
|
||||
}
|
||||
if len(graph.Steps) != 7 {
|
||||
t.Fatalf("expected 7 steps, got %d", len(graph.Steps))
|
||||
}
|
||||
|
||||
assertStep(t, graph.Steps[0], "edge.10_20.ledger.block", discovery.RailOperationBlock, discovery.RailLedger, model.ReportVisibilityHidden)
|
||||
assertStep(t, graph.Steps[1], "hop.10.ledger.fee", discovery.RailOperationFee, discovery.RailLedger, model.ReportVisibilityHidden)
|
||||
assertStep(t, graph.Steps[2], "hop.20.card_payout.send", discovery.RailOperationSend, discovery.RailCardPayout, model.ReportVisibilityUser)
|
||||
assertStep(t, graph.Steps[3], "hop.20.card_payout.observe", discovery.RailOperationObserveConfirm, discovery.RailCardPayout, model.ReportVisibilityUser)
|
||||
assertStep(t, graph.Steps[4], "edge.10_20.ledger.debit", discovery.RailOperationExternalDebit, discovery.RailLedger, model.ReportVisibilityHidden)
|
||||
assertStep(t, graph.Steps[5], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)
|
||||
assertStep(t, graph.Steps[6], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden)
|
||||
|
||||
if got, want := graph.Steps[1].DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) {
|
||||
t.Fatalf("fee deps mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
if got, want := graph.Steps[2].DependsOn, []string{graph.Steps[1].StepRef}; !equalStringSlice(got, want) {
|
||||
t.Fatalf("send deps mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompile_ExternalToInternal_UsesCreditAfterObserve(t *testing.T) {
|
||||
compiler := New()
|
||||
|
||||
|
||||
@@ -51,28 +51,28 @@ func (e *expansion) nextRef(base string) string {
|
||||
return token + "_" + itoa(count+1)
|
||||
}
|
||||
|
||||
func (e *expansion) needsWalletFeeStep(hop normalizedHop) bool {
|
||||
if e == nil || len(e.walletFeeHops) == 0 {
|
||||
func (e *expansion) needsFeeStep(hop normalizedHop) bool {
|
||||
if e == nil || len(e.feeHops) == 0 {
|
||||
return false
|
||||
}
|
||||
key := observedKey(hop)
|
||||
if _, ok := e.walletFeeHops[key]; !ok {
|
||||
if _, ok := e.feeHops[key]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, emitted := e.walletFeeEmitted[key]; emitted {
|
||||
if _, emitted := e.feeEmitted[key]; emitted {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *expansion) markWalletFeeEmitted(hop normalizedHop) {
|
||||
func (e *expansion) markFeeEmitted(hop normalizedHop) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
if e.walletFeeEmitted == nil {
|
||||
e.walletFeeEmitted = map[string]struct{}{}
|
||||
if e.feeEmitted == nil {
|
||||
e.feeEmitted = map[string]struct{}{}
|
||||
}
|
||||
e.walletFeeEmitted[observedKey(hop)] = struct{}{}
|
||||
e.feeEmitted[observedKey(hop)] = struct{}{}
|
||||
}
|
||||
|
||||
func normalizeStep(step Step) Step {
|
||||
|
||||
@@ -5,27 +5,26 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
func planWalletFeeHops(
|
||||
func planFeeHops(
|
||||
hops []normalizedHop,
|
||||
intent model.PaymentIntent,
|
||||
quote *model.PaymentQuoteSnapshot,
|
||||
) (map[string]struct{}, error) {
|
||||
_, hasWalletFee, err := walletFeeAmountFromSnapshots(intent, quote)
|
||||
_, hasFee, err := feeAmountFromSnapshots(intent, quote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasWalletFee {
|
||||
if !hasFee {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sourceHop, ok := sourceCryptoHop(hops)
|
||||
sourceHop, ok := sourceFeeHop(hops)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -34,19 +33,19 @@ func planWalletFeeHops(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sourceCryptoHop(hops []normalizedHop) (normalizedHop, bool) {
|
||||
func sourceFeeHop(hops []normalizedHop) (normalizedHop, bool) {
|
||||
for i := range hops {
|
||||
if hops[i].rail == discovery.RailCrypto && hops[i].role == paymenttypes.QuoteRouteHopRoleSource {
|
||||
if hops[i].role == paymenttypes.QuoteRouteHopRoleSource {
|
||||
return hops[i], true
|
||||
}
|
||||
}
|
||||
if len(hops) > 0 && hops[0].rail == discovery.RailCrypto {
|
||||
if len(hops) > 0 {
|
||||
return hops[0], true
|
||||
}
|
||||
return normalizedHop{}, false
|
||||
}
|
||||
|
||||
func walletFeeAmountFromSnapshots(
|
||||
func feeAmountFromSnapshots(
|
||||
intent model.PaymentIntent,
|
||||
quote *model.PaymentQuoteSnapshot,
|
||||
) (*paymenttypes.Money, bool, error) {
|
||||
@@ -62,10 +61,7 @@ func walletFeeAmountFromSnapshots(
|
||||
total := decimal.Zero
|
||||
currency := ""
|
||||
for i, line := range quote.FeeLines {
|
||||
if !isWalletDebitFeeLine(line) {
|
||||
continue
|
||||
}
|
||||
money := line.GetMoney()
|
||||
money := feeLineMoney(line)
|
||||
if money == nil {
|
||||
continue
|
||||
}
|
||||
@@ -80,7 +76,7 @@ func walletFeeAmountFromSnapshots(
|
||||
if currency == "" {
|
||||
currency = lineCurrency
|
||||
} else if !strings.EqualFold(currency, lineCurrency) {
|
||||
return nil, false, merrors.InvalidArgument("quote_snapshot.fee_lines wallet fee currency mismatch")
|
||||
return nil, false, merrors.InvalidArgument("quote_snapshot.fee_lines fee currency mismatch")
|
||||
}
|
||||
|
||||
amountRaw := strings.TrimSpace(money.GetAmount())
|
||||
@@ -88,10 +84,12 @@ func walletFeeAmountFromSnapshots(
|
||||
if err != nil {
|
||||
return nil, false, merrors.InvalidArgument(fmt.Sprintf("quote_snapshot.fee_lines[%d].money.amount is invalid", i))
|
||||
}
|
||||
if amount.Sign() < 0 {
|
||||
amount = amount.Neg()
|
||||
amount = amount.Abs()
|
||||
if amount.IsZero() {
|
||||
continue
|
||||
}
|
||||
if amount.Sign() == 0 {
|
||||
if line.GetSide() == paymenttypes.EntrySideCredit {
|
||||
total = total.Sub(amount)
|
||||
continue
|
||||
}
|
||||
total = total.Add(amount)
|
||||
@@ -113,16 +111,9 @@ func feePlanningSourceAmount(intent model.PaymentIntent, quote *model.PaymentQuo
|
||||
return intent.Amount
|
||||
}
|
||||
|
||||
func isWalletDebitFeeLine(line *paymenttypes.FeeLine) bool {
|
||||
func feeLineMoney(line *paymenttypes.FeeLine) *paymenttypes.Money {
|
||||
if line == nil {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
if line.GetSide() != paymenttypes.EntrySideDebit {
|
||||
return false
|
||||
}
|
||||
meta := line.Meta
|
||||
if len(meta) == 0 {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(meta["fee_target"]), "wallet")
|
||||
return line.GetMoney()
|
||||
}
|
||||
|
||||
@@ -30,16 +30,16 @@ type expansion struct {
|
||||
lastMainRef string
|
||||
refSeq map[string]int
|
||||
externalObserved map[string]string
|
||||
walletFeeHops map[string]struct{}
|
||||
walletFeeEmitted map[string]struct{}
|
||||
feeHops map[string]struct{}
|
||||
feeEmitted map[string]struct{}
|
||||
}
|
||||
|
||||
func newExpansion(walletFeeHops map[string]struct{}) *expansion {
|
||||
func newExpansion(feeHops map[string]struct{}) *expansion {
|
||||
return &expansion{
|
||||
refSeq: map[string]int{},
|
||||
externalObserved: map[string]string{},
|
||||
walletFeeHops: walletFeeHops,
|
||||
walletFeeEmitted: map[string]struct{}{},
|
||||
feeHops: feeHops,
|
||||
feeEmitted: map[string]struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +90,12 @@ func (s *svc) Compile(in Input) (graph *Graph, err error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
walletFeeHops, err := planWalletFeeHops(hops, in.IntentSnapshot, in.QuoteSnapshot)
|
||||
feeHops, err := planFeeHops(hops, in.IntentSnapshot, in.QuoteSnapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ex := newExpansion(walletFeeHops)
|
||||
ex := newExpansion(feeHops)
|
||||
appendGuards(ex, conditions)
|
||||
|
||||
if len(hops) == 1 {
|
||||
|
||||
@@ -18,7 +18,7 @@ func (s *svc) expandSingleHop(ex *expansion, hop normalizedHop, intent model.Pay
|
||||
|
||||
switch hop.role {
|
||||
case paymenttypes.QuoteRouteHopRoleSource:
|
||||
ex.appendMain(Step{
|
||||
appendMainWithOptionalFee(ex, Step{
|
||||
StepCode: singleHopCode(hop, "debit"),
|
||||
Kind: StepKindFundsDebit,
|
||||
Action: discovery.RailOperationDebit,
|
||||
@@ -26,7 +26,7 @@ func (s *svc) expandSingleHop(ex *expansion, hop normalizedHop, intent model.Pay
|
||||
HopIndex: hop.index,
|
||||
HopRole: hop.role,
|
||||
Visibility: model.ReportVisibilityHidden,
|
||||
})
|
||||
}, hop)
|
||||
case paymenttypes.QuoteRouteHopRoleDestination:
|
||||
ex.appendMain(Step{
|
||||
StepCode: singleHopCode(hop, "credit"),
|
||||
@@ -74,7 +74,7 @@ func (s *svc) applyDefaultBoundary(
|
||||
if to.rail == discovery.RailCardPayout && len(targets) > 0 {
|
||||
return s.applyBatchCardPayoutBoundary(ex, from, to, internalRail, intent, targets)
|
||||
}
|
||||
ex.appendMain(makeFundsBlockStep(from, to, internalRail))
|
||||
appendMainWithOptionalFee(ex, makeFundsBlockStep(from, to, internalRail), from)
|
||||
observeRef, err := s.ensureExternalObserved(ex, to, intent)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -97,7 +97,7 @@ func (s *svc) applyDefaultBoundary(
|
||||
return nil
|
||||
|
||||
case isInternalRail(from.rail) && isInternalRail(to.rail):
|
||||
ex.appendMain(makeFundsMoveStep(from, to, internalRailForBoundary(from, to)))
|
||||
appendMainWithOptionalFee(ex, makeFundsMoveStep(from, to, internalRailForBoundary(from, to)), from)
|
||||
return nil
|
||||
|
||||
default:
|
||||
@@ -113,7 +113,7 @@ func (s *svc) applyBatchCardPayoutBoundary(
|
||||
intent model.PaymentIntent,
|
||||
targets []batchmeta.PayoutTarget,
|
||||
) error {
|
||||
blockRef := ex.appendMain(makeFundsBlockStep(from, to, internalRail))
|
||||
blockRef := appendMainWithOptionalFee(ex, makeFundsBlockStep(from, to, internalRail), from)
|
||||
for i := range targets {
|
||||
target := targets[i]
|
||||
if target.Amount == nil {
|
||||
@@ -148,13 +148,13 @@ func (s *svc) ensureExternalObserved(ex *expansion, hop normalizedHop, intent mo
|
||||
sendStep := makeRailSendStep(hop, intent)
|
||||
sendRef := ex.appendMain(sendStep)
|
||||
observeDependency := sendRef
|
||||
if ex.needsWalletFeeStep(hop) {
|
||||
if ex.needsFeeStep(hop) {
|
||||
feeStep := makeRailFeeStep(hop)
|
||||
if observeDependency != "" {
|
||||
feeStep.DependsOn = []string{observeDependency}
|
||||
}
|
||||
observeDependency = ex.appendMain(feeStep)
|
||||
ex.markWalletFeeEmitted(hop)
|
||||
ex.markFeeEmitted(hop)
|
||||
}
|
||||
|
||||
observeStep := makeRailObserveStep(hop, intent)
|
||||
@@ -167,6 +167,20 @@ func (s *svc) ensureExternalObserved(ex *expansion, hop normalizedHop, intent mo
|
||||
return observeRef, nil
|
||||
}
|
||||
|
||||
func appendMainWithOptionalFee(ex *expansion, step Step, hop normalizedHop) string {
|
||||
mainRef := ex.appendMain(step)
|
||||
if !ex.needsFeeStep(hop) {
|
||||
return mainRef
|
||||
}
|
||||
feeStep := makeRailFeeStep(hop)
|
||||
if strings.TrimSpace(mainRef) != "" {
|
||||
feeStep.DependsOn = []string{mainRef}
|
||||
}
|
||||
feeRef := ex.appendMain(feeStep)
|
||||
ex.markFeeEmitted(hop)
|
||||
return feeRef
|
||||
}
|
||||
|
||||
func makeRailFeeStep(hop normalizedHop) Step {
|
||||
return Step{
|
||||
StepCode: singleHopCode(hop, "fee"),
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/qsnap"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
type orchestratorBatchMergeKeyResolver struct {
|
||||
cardGatewayRoutes map[string]CardGatewayRoute
|
||||
}
|
||||
|
||||
func newOrchestratorBatchMergeKeyResolver(routes map[string]CardGatewayRoute) psvc.BatchOptimizationMergeKeyResolver {
|
||||
return &orchestratorBatchMergeKeyResolver{
|
||||
cardGatewayRoutes: cloneCardGatewayRoutes(routes),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *orchestratorBatchMergeKeyResolver) ResolveMergeKey(
|
||||
item qsnap.ResolvedItem,
|
||||
selection psvc.BatchOptimizationSelection,
|
||||
) (string, bool) {
|
||||
if psvc.BatchOptimizationGrouping(strings.TrimSpace(string(selection.GroupBy))) != psvc.BatchOptimizationGroupingRailTarget {
|
||||
return "", false
|
||||
}
|
||||
if model.ParseRail(string(selection.MatchRail)) != discovery.RailCrypto {
|
||||
return "", false
|
||||
}
|
||||
return r.cryptoRailTargetKey(item)
|
||||
}
|
||||
|
||||
func (r *orchestratorBatchMergeKeyResolver) cryptoRailTargetKey(item qsnap.ResolvedItem) (string, bool) {
|
||||
destination := item.IntentSnapshot.Destination
|
||||
if destination.Type == model.EndpointTypeExternalChain && destination.ExternalChain != nil {
|
||||
return strings.Join([]string{
|
||||
"type=external_chain",
|
||||
"address=" + strings.TrimSpace(destination.ExternalChain.Address),
|
||||
"memo=" + strings.TrimSpace(destination.ExternalChain.Memo),
|
||||
}, "|"), true
|
||||
}
|
||||
if destination.Type == model.EndpointTypeManagedWallet && destination.ManagedWallet != nil {
|
||||
return strings.Join([]string{
|
||||
"type=managed_wallet",
|
||||
"wallet=" + strings.TrimSpace(destination.ManagedWallet.ManagedWalletRef),
|
||||
}, "|"), true
|
||||
}
|
||||
if destination.Type == model.EndpointTypeCard {
|
||||
address := r.resolveCardFundingAddress(item.QuoteSnapshot)
|
||||
if strings.TrimSpace(address) == "" {
|
||||
return "", false
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"type=external_chain",
|
||||
"address=" + strings.TrimSpace(address),
|
||||
"network=" + cryptoNetwork(item.QuoteSnapshot),
|
||||
}, "|"), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (r *orchestratorBatchMergeKeyResolver) resolveCardFundingAddress(snapshot *model.PaymentQuoteSnapshot) string {
|
||||
gatewayKey := destinationCardGatewayKeyFromQuote(snapshot)
|
||||
if gatewayKey == "" {
|
||||
return ""
|
||||
}
|
||||
route, ok := lookupCardGatewayRoute(r.cardGatewayRoutes, gatewayKey)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(route.FundingAddress)
|
||||
}
|
||||
|
||||
func destinationCardGatewayKeyFromQuote(snapshot *model.PaymentQuoteSnapshot) string {
|
||||
if snapshot == nil || snapshot.Route == nil {
|
||||
return ""
|
||||
}
|
||||
hops := snapshot.Route.Hops
|
||||
fallback := ""
|
||||
for i := range hops {
|
||||
hop := hops[i]
|
||||
if hop == nil || model.ParseRail(hop.Rail) != discovery.RailCardPayout {
|
||||
continue
|
||||
}
|
||||
key := firstNonEmpty(strings.TrimSpace(hop.Gateway), strings.TrimSpace(hop.InstanceID))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if hop.Role != paymenttypes.QuoteRouteHopRoleDestination {
|
||||
fallback = key
|
||||
continue
|
||||
}
|
||||
return key
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func cryptoNetwork(snapshot *model.PaymentQuoteSnapshot) string {
|
||||
if snapshot == nil || snapshot.Route == nil {
|
||||
return ""
|
||||
}
|
||||
for i := range snapshot.Route.Hops {
|
||||
hop := snapshot.Route.Hops[i]
|
||||
if hop == nil || model.ParseRail(hop.Rail) != discovery.RailCrypto {
|
||||
continue
|
||||
}
|
||||
network := strings.TrimSpace(hop.Network)
|
||||
if network != "" {
|
||||
return strings.ToUpper(network)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(snapshot.Route.Network) != "" {
|
||||
return strings.ToUpper(strings.TrimSpace(snapshot.Route.Network))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
amountMinor, currency, err := cardPayoutAmountMinor(req.Payment, req.Step.Metadata)
|
||||
amountMinor, currency, err := cardPayoutAmountMinor(req.Payment, req.StepExecution, req.Step.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -310,7 +310,10 @@ func payoutDestinationCard(payment *agg.Payment, metadata map[string]string) (*m
|
||||
return destination.Card, nil
|
||||
}
|
||||
|
||||
func cardPayoutMoney(payment *agg.Payment, metadata map[string]string) *paymenttypes.Money {
|
||||
func cardPayoutMoney(payment *agg.Payment, stepExecution agg.StepExecution, metadata map[string]string) *paymenttypes.Money {
|
||||
if planned := plannedStepMoney(stepExecution); planned != nil {
|
||||
return planned
|
||||
}
|
||||
if override, ok := batchmeta.AmountFromMetadata(metadata); ok && override != nil {
|
||||
return override
|
||||
}
|
||||
@@ -323,8 +326,8 @@ func cardPayoutMoney(payment *agg.Payment, metadata map[string]string) *paymentt
|
||||
return payment.IntentSnapshot.Amount
|
||||
}
|
||||
|
||||
func cardPayoutAmountMinor(payment *agg.Payment, metadata map[string]string) (int64, string, error) {
|
||||
money := cardPayoutMoney(payment, metadata)
|
||||
func cardPayoutAmountMinor(payment *agg.Payment, stepExecution agg.StepExecution, metadata map[string]string) (int64, string, error) {
|
||||
money := cardPayoutMoney(payment, stepExecution, metadata)
|
||||
if money == nil {
|
||||
return 0, "", merrors.InvalidArgument("card payout send: payout amount is required")
|
||||
}
|
||||
|
||||
@@ -176,6 +176,86 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesPlannedMoney(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
var payoutReq *mntxv1.CardPayoutRequest
|
||||
executor := &gatewayCardPayoutExecutor{
|
||||
gatewayRegistry: &fakeGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
ID: paymenttypes.DefaultCardsGatewayID,
|
||||
InstanceID: paymenttypes.DefaultCardsGatewayID,
|
||||
Rail: discovery.RailCardPayout,
|
||||
InvokeURI: "grpc://mntx-gateway:50051",
|
||||
IsEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
dialClient: func(_ context.Context, _ string) (mntxclient.Client, error) {
|
||||
return &mntxclient.Fake{
|
||||
CreateCardPayoutFn: func(_ context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
||||
payoutReq = req
|
||||
return &mntxv1.CardPayoutResponse{
|
||||
Payout: &mntxv1.CardPayoutState{
|
||||
PayoutId: "payout-planned",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := executor.ExecuteCardPayout(context.Background(), sexec.StepRequest{
|
||||
Payment: &agg.Payment{
|
||||
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
|
||||
PaymentRef: "payment-planned",
|
||||
IdempotencyKey: "idem-planned",
|
||||
IntentSnapshot: model.PaymentIntent{
|
||||
Ref: "intent-planned",
|
||||
Destination: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeCard,
|
||||
Card: &model.CardEndpoint{
|
||||
Pan: "2200700142860161",
|
||||
ExpMonth: 3,
|
||||
ExpYear: 2030,
|
||||
},
|
||||
},
|
||||
Amount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
|
||||
},
|
||||
QuoteSnapshot: &model.PaymentQuoteSnapshot{
|
||||
ExpectedSettlementAmount: &paymenttypes.Money{Amount: "76.50", Currency: "RUB"},
|
||||
},
|
||||
},
|
||||
Step: xplan.Step{
|
||||
StepRef: "hop_4_card_payout_send",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
Action: discovery.RailOperationSend,
|
||||
Rail: discovery.RailCardPayout,
|
||||
Gateway: paymenttypes.DefaultCardsGatewayID,
|
||||
InstanceID: paymenttypes.DefaultCardsGatewayID,
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "hop_4_card_payout_send",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
Attempt: 1,
|
||||
PlannedMoney: &paymenttypes.Money{Amount: "100.00", Currency: "RUB"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteCardPayout returned error: %v", err)
|
||||
}
|
||||
if payoutReq == nil {
|
||||
t.Fatal("expected payout request to be submitted")
|
||||
}
|
||||
if got, want := payoutReq.GetAmountMinor(), int64(10000); got != want {
|
||||
t.Fatalf("amount_minor mismatch: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := payoutReq.GetCurrency(), "RUB"; got != want {
|
||||
t.Fatalf("currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesStepMetadataOverrides(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func (e *gatewayCryptoExecutor) ExecuteCrypto(ctx context.Context, req sexec.Ste
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
amount, err := sourceAmount(req.Payment, action)
|
||||
amount, err := sourceAmount(req.Payment, req.StepExecution, action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -170,19 +170,26 @@ func sourceManagedWalletRef(payment *agg.Payment) (string, error) {
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func sourceAmount(payment *agg.Payment, action model.RailOperation) (*moneyv1.Money, error) {
|
||||
func sourceAmount(payment *agg.Payment, stepExecution agg.StepExecution, action model.RailOperation) (*moneyv1.Money, error) {
|
||||
if planned := plannedStepMoney(stepExecution); planned != nil {
|
||||
proto := paymentMoneyToProto(planned)
|
||||
if proto == nil {
|
||||
return nil, merrors.InvalidArgument("crypto send: planned amount is invalid")
|
||||
}
|
||||
return proto, nil
|
||||
}
|
||||
if payment == nil {
|
||||
return nil, merrors.InvalidArgument("crypto send: payment is required")
|
||||
}
|
||||
var money *paymenttypes.Money
|
||||
switch action {
|
||||
case discovery.RailOperationFee:
|
||||
resolved, ok, err := walletFeeAmount(payment)
|
||||
resolved, ok, err := feeAmount(payment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, merrors.InvalidArgument("crypto send: wallet fee amount is required")
|
||||
return nil, merrors.InvalidArgument("crypto send: fee amount is required")
|
||||
}
|
||||
money = resolved
|
||||
default:
|
||||
@@ -212,7 +219,7 @@ func effectiveSourceAmount(payment *agg.Payment) *paymenttypes.Money {
|
||||
return payment.IntentSnapshot.Amount
|
||||
}
|
||||
|
||||
func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) {
|
||||
func feeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) {
|
||||
if payment == nil || payment.QuoteSnapshot == nil || len(payment.QuoteSnapshot.FeeLines) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
@@ -225,9 +232,6 @@ func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) {
|
||||
total := decimal.Zero
|
||||
currency := ""
|
||||
for i, line := range payment.QuoteSnapshot.FeeLines {
|
||||
if !isWalletDebitFeeLine(line) {
|
||||
continue
|
||||
}
|
||||
money := line.GetMoney()
|
||||
if money == nil {
|
||||
continue
|
||||
@@ -243,7 +247,7 @@ func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) {
|
||||
if currency == "" {
|
||||
currency = lineCurrency
|
||||
} else if !strings.EqualFold(currency, lineCurrency) {
|
||||
return nil, false, merrors.InvalidArgument("crypto send: wallet fee currency mismatch")
|
||||
return nil, false, merrors.InvalidArgument("crypto send: fee currency mismatch")
|
||||
}
|
||||
|
||||
amountRaw := strings.TrimSpace(money.GetAmount())
|
||||
@@ -251,10 +255,12 @@ func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) {
|
||||
if err != nil {
|
||||
return nil, false, merrors.InvalidArgument(fmt.Sprintf("crypto send: fee_lines[%d].money.amount is invalid", i))
|
||||
}
|
||||
if amount.Sign() < 0 {
|
||||
amount = amount.Neg()
|
||||
amount = amount.Abs()
|
||||
if amount.IsZero() {
|
||||
continue
|
||||
}
|
||||
if amount.Sign() == 0 {
|
||||
if line.GetSide() == paymenttypes.EntrySideCredit {
|
||||
total = total.Sub(amount)
|
||||
continue
|
||||
}
|
||||
total = total.Add(amount)
|
||||
@@ -269,20 +275,6 @@ func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) {
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func isWalletDebitFeeLine(line *paymenttypes.FeeLine) bool {
|
||||
if line == nil {
|
||||
return false
|
||||
}
|
||||
if line.GetSide() != paymenttypes.EntrySideDebit {
|
||||
return false
|
||||
}
|
||||
meta := line.Meta
|
||||
if len(meta) == 0 {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(meta["fee_target"]), "wallet")
|
||||
}
|
||||
|
||||
func (e *gatewayCryptoExecutor) resolveDestination(
|
||||
ctx context.Context,
|
||||
client chainclient.Client,
|
||||
|
||||
@@ -138,6 +138,97 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsTransfer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayCryptoExecutor_ExecuteCrypto_UsesPlannedMoney(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
var submitReq *chainv1.SubmitTransferRequest
|
||||
client := &chainclient.Fake{
|
||||
SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
submitReq = req
|
||||
return &chainv1.SubmitTransferResponse{
|
||||
Transfer: &chainv1.Transfer{
|
||||
TransferRef: "trf-planned",
|
||||
OperationRef: "op-planned",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
executor := &gatewayCryptoExecutor{
|
||||
gatewayInvokeResolver: &fakeGatewayInvokeResolver{client: client},
|
||||
gatewayRegistry: &fakeGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
ID: "crypto_rail_gateway_arbitrum_sepolia",
|
||||
InstanceID: "crypto_rail_gateway_arbitrum_sepolia",
|
||||
Rail: discovery.RailCrypto,
|
||||
InvokeURI: "grpc://crypto-gateway",
|
||||
IsEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cardGatewayRoutes: map[string]CardGatewayRoute{
|
||||
paymenttypes.DefaultCardsGatewayID: {FundingAddress: "TUA_DEST"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := executor.ExecuteCrypto(context.Background(), sexec.StepRequest{
|
||||
Payment: &agg.Payment{
|
||||
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
|
||||
PaymentRef: "payment-planned",
|
||||
IdempotencyKey: "idem-planned",
|
||||
IntentSnapshot: model.PaymentIntent{
|
||||
Ref: "intent-planned",
|
||||
Source: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeManagedWallet,
|
||||
ManagedWallet: &model.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: "wallet-src",
|
||||
},
|
||||
},
|
||||
Destination: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeCard,
|
||||
Card: &model.CardEndpoint{Pan: "4111111111111111"},
|
||||
},
|
||||
Amount: &paymenttypes.Money{Amount: "1", Currency: "USDT"},
|
||||
},
|
||||
QuoteSnapshot: &model.PaymentQuoteSnapshot{
|
||||
DebitAmount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
|
||||
Route: &paymenttypes.QuoteRouteSpecification{
|
||||
Hops: []*paymenttypes.QuoteRouteHop{
|
||||
{Index: 1, Rail: "CRYPTO", Gateway: "crypto_rail_gateway_arbitrum_sepolia", InstanceID: "crypto_rail_gateway_arbitrum_sepolia", Role: paymenttypes.QuoteRouteHopRoleSource},
|
||||
{Index: 4, Rail: "CARD", Gateway: paymenttypes.DefaultCardsGatewayID, InstanceID: paymenttypes.DefaultCardsGatewayID, Role: paymenttypes.QuoteRouteHopRoleDestination},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Step: xplan.Step{
|
||||
StepRef: "hop_1_crypto_send",
|
||||
StepCode: "hop.1.crypto.send",
|
||||
Action: discovery.RailOperationSend,
|
||||
Rail: discovery.RailCrypto,
|
||||
Gateway: "crypto_rail_gateway_arbitrum_sepolia",
|
||||
InstanceID: "crypto_rail_gateway_arbitrum_sepolia",
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "hop_1_crypto_send",
|
||||
StepCode: "hop.1.crypto.send",
|
||||
Attempt: 1,
|
||||
PlannedMoney: &paymenttypes.Money{Amount: "5.50", Currency: "USDT"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteCrypto returned error: %v", err)
|
||||
}
|
||||
if submitReq == nil {
|
||||
t.Fatal("expected transfer submission request")
|
||||
}
|
||||
if got, want := submitReq.GetAmount().GetAmount(), "5.50"; got != want {
|
||||
t.Fatalf("amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := submitReq.GetAmount().GetCurrency(), "USDT"; got != want {
|
||||
t.Fatalf("currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayCryptoExecutor_ExecuteCrypto_MissingCardRoute(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
executor := &gatewayCryptoExecutor{
|
||||
@@ -430,7 +521,7 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionResolvesFeeAddressFromFeeW
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesWalletFeeAmount(t *testing.T) {
|
||||
func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesAllFeeLines(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
var submitReq *chainv1.SubmitTransferRequest
|
||||
@@ -493,6 +584,18 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesWalletFeeAmount(t *tes
|
||||
Side: paymenttypes.EntrySideDebit,
|
||||
Meta: map[string]string{"fee_target": "wallet"},
|
||||
},
|
||||
{
|
||||
Money: &paymenttypes.Money{Amount: "0.30", Currency: "USDT"},
|
||||
LineType: paymenttypes.PostingLineTypeTax,
|
||||
Side: paymenttypes.EntrySideDebit,
|
||||
Meta: map[string]string{"fee_target": "ledger"},
|
||||
},
|
||||
{
|
||||
Money: &paymenttypes.Money{Amount: "0.10", Currency: "USDT"},
|
||||
LineType: paymenttypes.PostingLineTypeReversal,
|
||||
Side: paymenttypes.EntrySideCredit,
|
||||
Meta: map[string]string{"fee_target": "rebate"},
|
||||
},
|
||||
},
|
||||
Route: &paymenttypes.QuoteRouteSpecification{
|
||||
Hops: []*paymenttypes.QuoteRouteHop{
|
||||
@@ -524,7 +627,7 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesWalletFeeAmount(t *tes
|
||||
if submitReq == nil {
|
||||
t.Fatal("expected transfer submission")
|
||||
}
|
||||
if got, want := submitReq.GetAmount().GetAmount(), "0.7"; got != want {
|
||||
if got, want := submitReq.GetAmount().GetAmount(), "0.9"; got != want {
|
||||
t.Fatalf("fee amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := submitReq.GetDestination().GetExternalAddress(), "TUA_FEE"; got != want {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||
paymentgatewaynotifications "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
||||
@@ -159,7 +160,7 @@ func buildGatewayExecutionEvent(payment *agg.Payment, msg *pmodel.PaymentGateway
|
||||
TransferRef: transferRef,
|
||||
GatewayInstanceID: gatewayInstanceID,
|
||||
Status: status,
|
||||
ExecutedMoney: clonePaymentMoney(msg.ExecutedMoney),
|
||||
ExecutedMoney: svcshared.CloneMoneyTrimNonEmpty(msg.ExecutedMoney),
|
||||
}
|
||||
|
||||
switch status {
|
||||
|
||||
@@ -56,7 +56,7 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste
|
||||
return nil, merrors.InvalidArgument("ledger step: unsupported action")
|
||||
}
|
||||
|
||||
amount, err := ledgerAmountForStep(req.Payment, req.Step, action)
|
||||
amount, err := ledgerAmountForStep(req.Payment, req.Step, req.StepExecution, action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -132,8 +132,12 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste
|
||||
func ledgerAmountForStep(
|
||||
payment *agg.Payment,
|
||||
step xplan.Step,
|
||||
stepExecution agg.StepExecution,
|
||||
action model.RailOperation,
|
||||
) (*moneyv1.Money, error) {
|
||||
if planned := plannedStepMoney(stepExecution); planned != nil {
|
||||
return protoMoneyRequired(planned, "ledger step: planned amount is invalid")
|
||||
}
|
||||
if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok && override != nil {
|
||||
return protoMoneyRequired(override, "ledger step: override amount is invalid")
|
||||
}
|
||||
|
||||
@@ -95,6 +95,49 @@ func TestGatewayLedgerExecutor_ExecuteLedger_CreditUsesSourceAmountAndDefaultRol
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_UsesPlannedMoney(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
var transferReq *ledgerv1.TransferRequest
|
||||
executor := &gatewayLedgerExecutor{
|
||||
ledgerClient: &ledgerclient.Fake{
|
||||
TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||
transferReq = req
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-planned"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
|
||||
Payment: payment,
|
||||
Step: xplan.Step{
|
||||
StepRef: "edge_1_2_ledger_credit",
|
||||
StepCode: "edge.1_2.ledger.credit",
|
||||
Action: discovery.RailOperationCredit,
|
||||
Rail: discovery.RailLedger,
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "edge_1_2_ledger_credit",
|
||||
StepCode: "edge.1_2.ledger.credit",
|
||||
Attempt: 1,
|
||||
PlannedMoney: &paymenttypes.Money{Amount: "12.34", Currency: "EUR"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteLedger returned error: %v", err)
|
||||
}
|
||||
if transferReq == nil {
|
||||
t.Fatal("expected ledger transfer request")
|
||||
}
|
||||
if got, want := transferReq.GetMoney().GetAmount(), "12.34"; got != want {
|
||||
t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := transferReq.GetMoney().GetCurrency(), "EUR"; got != want {
|
||||
t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditUsesPostCreditWithCharges(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
@@ -71,12 +71,17 @@ func newOrchestrationV2Service(logger mlogger.Logger, repo storage.Repository, r
|
||||
executors := buildOrchestrationV2Executors(logger, runtimeDeps)
|
||||
|
||||
svc, err := psvc.New(psvc.Dependencies{
|
||||
Logger: logger.Named("v2"),
|
||||
QuoteStore: repo.Quotes(),
|
||||
Repository: paymentRepo,
|
||||
Query: query,
|
||||
Observer: observer,
|
||||
Executors: executors,
|
||||
Logger: logger.Named("v2"),
|
||||
QuoteStore: repo.Quotes(),
|
||||
Repository: paymentRepo,
|
||||
Query: query,
|
||||
Observer: observer,
|
||||
Executors: executors,
|
||||
BatchOptimizer: psvc.NewPolicyBatchOptimizer(psvc.PolicyBatchOptimizerDependencies{
|
||||
Logger: logger.Named("v2"),
|
||||
Policy: runtimeDeps.BatchOptimizationPolicy,
|
||||
MergeKeyResolver: newOrchestratorBatchMergeKeyResolver(runtimeDeps.CardGatewayRoutes),
|
||||
}),
|
||||
BatchOptimizationPolicy: runtimeDeps.BatchOptimizationPolicy,
|
||||
Producer: runtimeDeps.Producer,
|
||||
})
|
||||
|
||||
@@ -2,13 +2,14 @@ package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
@@ -52,11 +53,11 @@ func (e *gatewayProviderSettlementExecutor) ExecuteProviderSettlement(ctx contex
|
||||
},
|
||||
}
|
||||
|
||||
amount, err := settlementAmount(req.Payment)
|
||||
amount, err := settlementAmount(req.Payment, req.StepExecution)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
convertedAmount := settlementConvertedMoney(req.Payment)
|
||||
convertedAmount := settlementConvertedMoney(req.Payment, req.StepExecution)
|
||||
if convertedAmount == nil {
|
||||
return nil, merrors.InvalidArgument("settlement fx_convert: converted amount is required")
|
||||
}
|
||||
@@ -106,11 +107,14 @@ func (e *gatewayProviderSettlementExecutor) ExecuteProviderSettlement(ctx contex
|
||||
return &sexec.ExecuteOutput{StepExecution: step}, nil
|
||||
}
|
||||
|
||||
func settlementConvertedMoney(payment *agg.Payment) *paymenttypes.Money {
|
||||
func settlementConvertedMoney(payment *agg.Payment, stepExecution agg.StepExecution) *paymenttypes.Money {
|
||||
if planned := plannedStepConvertedMoney(stepExecution); planned != nil {
|
||||
return planned
|
||||
}
|
||||
if payment == nil || payment.QuoteSnapshot == nil {
|
||||
return nil
|
||||
}
|
||||
return clonePaymentMoney(payment.QuoteSnapshot.ExpectedSettlementAmount)
|
||||
return svcshared.CloneMoneyTrimNonEmpty(payment.QuoteSnapshot.ExpectedSettlementAmount)
|
||||
}
|
||||
|
||||
func (e *gatewayProviderSettlementExecutor) resolveGateway(ctx context.Context, step xplan.Step) (*model.GatewayInstanceDescriptor, error) {
|
||||
@@ -165,7 +169,14 @@ func (e *gatewayProviderSettlementExecutor) resolveGateway(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
func settlementAmount(payment *agg.Payment) (*moneyv1.Money, error) {
|
||||
func settlementAmount(payment *agg.Payment, stepExecution agg.StepExecution) (*moneyv1.Money, error) {
|
||||
if planned := plannedStepMoney(stepExecution); planned != nil {
|
||||
amount := paymentMoneyToProto(planned)
|
||||
if amount == nil {
|
||||
return nil, merrors.InvalidArgument("settlement fx_convert: planned amount is invalid")
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
if payment == nil {
|
||||
return nil, merrors.InvalidArgument("settlement fx_convert: payment is required")
|
||||
}
|
||||
|
||||
@@ -151,6 +151,96 @@ func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_SubmitsTran
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_UsesPlannedMoney(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
var submitReq *chainv1.SubmitTransferRequest
|
||||
executor := &gatewayProviderSettlementExecutor{
|
||||
gatewayInvokeResolver: &fakeGatewayInvokeResolver{
|
||||
client: &chainclient.Fake{
|
||||
SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
submitReq = req
|
||||
return &chainv1.SubmitTransferResponse{
|
||||
Transfer: &chainv1.Transfer{
|
||||
TransferRef: "trf-planned",
|
||||
OperationRef: "op-planned",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
gatewayRegistry: &fakeGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
ID: "payment_gateway_settlement",
|
||||
InstanceID: "payment_gateway_settlement",
|
||||
Rail: discovery.RailProviderSettlement,
|
||||
InvokeURI: "grpc://tgsettle-gateway",
|
||||
IsEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := executor.ExecuteProviderSettlement(context.Background(), sexec.StepRequest{
|
||||
Payment: &agg.Payment{
|
||||
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
|
||||
PaymentRef: "payment-planned",
|
||||
IdempotencyKey: "idem-planned",
|
||||
IntentSnapshot: model.PaymentIntent{
|
||||
Ref: "intent-planned",
|
||||
Source: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeManagedWallet,
|
||||
ManagedWallet: &model.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: "wallet-src",
|
||||
},
|
||||
},
|
||||
Amount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
|
||||
},
|
||||
QuoteSnapshot: &model.PaymentQuoteSnapshot{
|
||||
DebitAmount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
|
||||
ExpectedSettlementAmount: &paymenttypes.Money{Amount: "76.63", Currency: "RUB"},
|
||||
},
|
||||
},
|
||||
Step: xplan.Step{
|
||||
StepRef: "hop_2_settlement_fx_convert",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
Action: discovery.RailOperationFXConvert,
|
||||
Rail: discovery.RailProviderSettlement,
|
||||
Gateway: "payment_gateway_settlement",
|
||||
InstanceID: "payment_gateway_settlement",
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "hop_2_settlement_fx_convert",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
Attempt: 1,
|
||||
PlannedMoney: &paymenttypes.Money{Amount: "2.00", Currency: "USDT"},
|
||||
PlannedConvertedMoney: &paymenttypes.Money{Amount: "150.00", Currency: "RUB"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteProviderSettlement returned error: %v", err)
|
||||
}
|
||||
if submitReq == nil {
|
||||
t.Fatal("expected transfer submission request")
|
||||
}
|
||||
if got, want := submitReq.GetAmount().GetAmount(), "2.00"; got != want {
|
||||
t.Fatalf("amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := submitReq.GetAmount().GetCurrency(), "USDT"; got != want {
|
||||
t.Fatalf("currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if out == nil || out.StepExecution.ConvertedMoney == nil {
|
||||
t.Fatal("expected converted money in executor output")
|
||||
}
|
||||
if got, want := out.StepExecution.ConvertedMoney.Amount, "150.00"; got != want {
|
||||
t.Fatalf("converted amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.ConvertedMoney.Currency, "RUB"; got != want {
|
||||
t.Fatalf("converted currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_MissingSettlementAmount(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
@@ -11,21 +13,6 @@ import (
|
||||
|
||||
var cardMinorUnitScale = decimal.NewFromInt(100)
|
||||
|
||||
func clonePaymentMoney(src *paymenttypes.Money) *paymenttypes.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(src.GetAmount())
|
||||
currency := strings.TrimSpace(src.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func protoMoneyToPaymentMoney(src *moneyv1.Money) *paymenttypes.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
@@ -38,6 +25,26 @@ func protoMoneyToPaymentMoney(src *moneyv1.Money) *paymenttypes.Money {
|
||||
return &paymenttypes.Money{Amount: amount, Currency: currency}
|
||||
}
|
||||
|
||||
func paymentMoneyToProto(src *paymenttypes.Money) *moneyv1.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(src.GetAmount())
|
||||
currency := strings.TrimSpace(src.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{Amount: amount, Currency: currency}
|
||||
}
|
||||
|
||||
func plannedStepMoney(step agg.StepExecution) *paymenttypes.Money {
|
||||
return svcshared.CloneMoneyTrimNonEmpty(step.PlannedMoney)
|
||||
}
|
||||
|
||||
func plannedStepConvertedMoney(step agg.StepExecution) *paymenttypes.Money {
|
||||
return svcshared.CloneMoneyTrimNonEmpty(step.PlannedConvertedMoney)
|
||||
}
|
||||
|
||||
func transferExecutedMoney(transfer *chainv1.Transfer) *paymenttypes.Money {
|
||||
if transfer == nil {
|
||||
return nil
|
||||
|
||||
24
api/payments/orchestrator/internal/service/shared/money.go
Normal file
24
api/payments/orchestrator/internal/service/shared/money.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
// CloneMoneyTrimNonEmpty clones a money value after trimming fields.
|
||||
// It returns nil when amount or currency is missing.
|
||||
func CloneMoneyTrimNonEmpty(src *paymenttypes.Money) *paymenttypes.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(src.GetAmount())
|
||||
currency := strings.TrimSpace(src.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,6 @@ require (
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
|
||||
)
|
||||
|
||||
@@ -202,8 +202,8 @@ 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
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=
|
||||
@@ -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-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/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/payments/quotation/internal/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
@@ -23,10 +24,7 @@ type moneyGetter interface {
|
||||
}
|
||||
|
||||
func cloneMoney(input *paymenttypes.Money) *paymenttypes.Money {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{Currency: input.GetCurrency(), Amount: input.GetAmount()}
|
||||
return shared.CloneModelMoneyRaw(input)
|
||||
}
|
||||
|
||||
func cloneStringList(values []string) []string {
|
||||
@@ -100,10 +98,7 @@ func protoMoney(m *paymenttypes.Money) *moneyv1.Money {
|
||||
}
|
||||
|
||||
func cloneProtoMoney(input *moneyv1.Money) *moneyv1.Money {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{Currency: input.GetCurrency(), Amount: input.GetAmount()}
|
||||
return shared.CloneProtoMoneyRaw(input)
|
||||
}
|
||||
|
||||
func decimalFromProto(value *moneyv1.Decimal) *paymenttypes.Decimal {
|
||||
|
||||
@@ -67,13 +67,7 @@ func BuildFundingGateFromProfile(
|
||||
}
|
||||
|
||||
func cloneProtoMoney(src *moneyv1.Money) *moneyv1.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{
|
||||
Amount: strings.TrimSpace(src.GetAmount()),
|
||||
Currency: strings.TrimSpace(src.GetCurrency()),
|
||||
}
|
||||
return shared.CloneProtoMoneyTrim(src)
|
||||
}
|
||||
|
||||
func clonePaymentEndpoint(src *model.PaymentEndpoint) *model.PaymentEndpoint {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||
qshared "github.com/tech/sendico/payments/quotation/internal/shared"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -31,13 +32,7 @@ const (
|
||||
)
|
||||
|
||||
func cloneProtoMoney(input *moneyv1.Money) *moneyv1.Money {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{
|
||||
Currency: input.GetCurrency(),
|
||||
Amount: input.GetAmount(),
|
||||
}
|
||||
return qshared.CloneProtoMoneyRaw(input)
|
||||
}
|
||||
|
||||
func cloneMetadata(input map[string]string) map[string]string {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
qshared "github.com/tech/sendico/payments/quotation/internal/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
payecon "github.com/tech/sendico/pkg/payments/economics"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
@@ -38,13 +39,7 @@ func firstNonEmpty(values ...string) string {
|
||||
}
|
||||
|
||||
func cloneProtoMoney(src *moneyv1.Money) *moneyv1.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{
|
||||
Amount: strings.TrimSpace(src.GetAmount()),
|
||||
Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())),
|
||||
}
|
||||
return qshared.CloneProtoMoneyTrimUpperCurrency(src)
|
||||
}
|
||||
|
||||
func resolvedSettlementModeFromModel(mode model.SettlementMode) paymentv1.SettlementMode {
|
||||
|
||||
@@ -3,19 +3,14 @@ package quote_computation_service
|
||||
import (
|
||||
"strings"
|
||||
|
||||
qshared "github.com/tech/sendico/payments/quotation/internal/shared"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
)
|
||||
|
||||
func cloneProtoMoney(src *moneyv1.Money) *moneyv1.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{
|
||||
Amount: strings.TrimSpace(src.GetAmount()),
|
||||
Currency: strings.TrimSpace(src.GetCurrency()),
|
||||
}
|
||||
return qshared.CloneProtoMoneyTrim(src)
|
||||
}
|
||||
|
||||
func protoMoneyFromModel(src *paymenttypes.Money) *moneyv1.Money {
|
||||
@@ -58,13 +53,7 @@ func cloneAsset(src *paymenttypes.Asset) *paymenttypes.Asset {
|
||||
}
|
||||
|
||||
func cloneModelMoney(src *paymenttypes.Money) *paymenttypes.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: strings.TrimSpace(src.GetAmount()),
|
||||
Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())),
|
||||
}
|
||||
return qshared.CloneModelMoneyTrimUpperCurrency(src)
|
||||
}
|
||||
|
||||
func clonePaymentIntent(src model.PaymentIntent) model.PaymentIntent {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user