Added debit settlement amount calculation
This commit is contained in:
@@ -51,7 +51,7 @@ require (
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
|
||||
@@ -43,8 +43,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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
|
||||
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
|
||||
@@ -112,6 +112,7 @@ func quoteSnapshotToModel(src *orchestratorv1.PaymentQuote) *model.PaymentQuoteS
|
||||
}
|
||||
return &model.PaymentQuoteSnapshot{
|
||||
DebitAmount: moneyFromProto(src.GetDebitAmount()),
|
||||
DebitSettlementAmount: moneyFromProto(src.GetDebitSettlementAmount()),
|
||||
ExpectedSettlementAmount: moneyFromProto(src.GetExpectedSettlementAmount()),
|
||||
ExpectedFeeTotal: moneyFromProto(src.GetExpectedFeeTotal()),
|
||||
FeeLines: feeLinesFromProto(src.GetFeeLines()),
|
||||
@@ -389,6 +390,7 @@ func modelQuoteToProto(src *model.PaymentQuoteSnapshot) *orchestratorv1.PaymentQ
|
||||
}
|
||||
return &orchestratorv1.PaymentQuote{
|
||||
DebitAmount: protoMoney(src.DebitAmount),
|
||||
DebitSettlementAmount: protoMoney(src.DebitSettlementAmount),
|
||||
ExpectedSettlementAmount: protoMoney(src.ExpectedSettlementAmount),
|
||||
ExpectedFeeTotal: protoMoney(src.ExpectedFeeTotal),
|
||||
FeeLines: feeLinesToProto(src.FeeLines),
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/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
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/storage"
|
||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
@@ -11,31 +10,6 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func analyzeExecutionPlan(plan *model.ExecutionPlan) (done bool, failed bool, rootErr error) {
|
||||
if plan == nil || len(plan.Steps) == 0 {
|
||||
return true, false, nil
|
||||
}
|
||||
|
||||
done = true
|
||||
for _, s := range plan.Steps {
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.State == model.OperationStateFailed {
|
||||
failed = true
|
||||
if rootErr == nil && s.Error != "" {
|
||||
rootErr = errors.New(s.Error)
|
||||
}
|
||||
}
|
||||
|
||||
if !isStepFinal(s) { // created/waiting/processing
|
||||
done = false
|
||||
}
|
||||
}
|
||||
return done, failed, rootErr
|
||||
}
|
||||
|
||||
func buildStepIndex(plan *model.PaymentPlan) map[string]int {
|
||||
m := make(map[string]int, len(plan.Steps))
|
||||
for i, s := range plan.Steps {
|
||||
@@ -63,64 +37,6 @@ func isStepFinal(step *model.ExecutionStep) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func stepCodeIsDependent(code string, previousSteps []string) bool {
|
||||
for _, ps := range previousSteps {
|
||||
if ps == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stepIsIndependent(
|
||||
step *model.ExecutionStep,
|
||||
plan *model.PaymentPlan,
|
||||
execSteps map[string]*model.ExecutionStep,
|
||||
) bool {
|
||||
|
||||
for _, s := range plan.Steps {
|
||||
if s.StepID != step.Code {
|
||||
continue
|
||||
}
|
||||
|
||||
// Do not process step if it is already in a final state
|
||||
if isStepFinal(step) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the step has no dependencies, it is independent
|
||||
if len(s.DependsOn) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// All dependent steps must be successfully completed
|
||||
for _, dep := range s.DependsOn {
|
||||
depStep := execSteps[dep]
|
||||
if depStep == nil || depStep.State != model.OperationStateSuccess {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func planStep(execStep *model.ExecutionStep, plan *model.PaymentPlan) *model.PaymentStep {
|
||||
if (execStep == nil) || (plan == nil) {
|
||||
return nil
|
||||
}
|
||||
for _, step := range plan.Steps {
|
||||
if step != nil {
|
||||
if step.StepID == execStep.Code {
|
||||
return step
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *paymentExecutor) pickIndependentSteps(
|
||||
ctx context.Context,
|
||||
l *zap.Logger,
|
||||
@@ -262,14 +178,13 @@ func (p *paymentExecutor) executePaymentPlan(
|
||||
// Execute steps
|
||||
if err := p.pickWaitingSteps(ctx, logger, store, payment, quote); err != nil {
|
||||
logger.Warn("Step execution returned infrastructure error", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.Update(ctx, payment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
done, failed, rootErr := analyzeExecutionPlan(payment.ExecutionPlan)
|
||||
done, failed, rootErr := analyzeExecutionPlan(logger, payment)
|
||||
if !done {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,74 +1,12 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
func planExecutionOrder(plan *model.PaymentPlan) ([]int, map[string]int, error) {
|
||||
if plan == nil || len(plan.Steps) == 0 {
|
||||
return nil, nil, merrors.InvalidArgument("payment plan: steps are required")
|
||||
}
|
||||
idToIndex := map[string]int{}
|
||||
for idx, step := range plan.Steps {
|
||||
if step == nil {
|
||||
return nil, nil, merrors.InvalidArgument("payment plan: step is required")
|
||||
}
|
||||
id := planStepID(step, idx)
|
||||
if _, exists := idToIndex[id]; exists {
|
||||
return nil, nil, merrors.InvalidArgument("payment plan: duplicate step id")
|
||||
}
|
||||
idToIndex[id] = idx
|
||||
}
|
||||
|
||||
indegree := make([]int, len(plan.Steps))
|
||||
adj := make([][]int, len(plan.Steps))
|
||||
for idx, step := range plan.Steps {
|
||||
for _, dep := range step.DependsOn {
|
||||
key := strings.TrimSpace(dep)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
depIdx, ok := idToIndex[key]
|
||||
if !ok {
|
||||
return nil, nil, merrors.InvalidArgument("payment plan: dependency missing")
|
||||
}
|
||||
adj[depIdx] = append(adj[depIdx], idx)
|
||||
indegree[idx]++
|
||||
}
|
||||
}
|
||||
|
||||
queue := make([]int, 0, len(plan.Steps))
|
||||
for idx := range indegree {
|
||||
if indegree[idx] == 0 {
|
||||
queue = append(queue, idx)
|
||||
}
|
||||
}
|
||||
sort.Ints(queue)
|
||||
|
||||
order := make([]int, 0, len(plan.Steps))
|
||||
for len(queue) > 0 {
|
||||
current := queue[0]
|
||||
queue = queue[1:]
|
||||
order = append(order, current)
|
||||
for _, next := range adj[current] {
|
||||
indegree[next]--
|
||||
if indegree[next] == 0 {
|
||||
queue = append(queue, next)
|
||||
}
|
||||
}
|
||||
sort.Ints(queue)
|
||||
}
|
||||
|
||||
if len(order) != len(plan.Steps) {
|
||||
return nil, nil, merrors.InvalidArgument("payment plan: dependency cycle detected")
|
||||
}
|
||||
return order, idToIndex, nil
|
||||
}
|
||||
|
||||
func executionStepsByCode(plan *model.ExecutionPlan) map[string]*model.ExecutionStep {
|
||||
result := map[string]*model.ExecutionStep{}
|
||||
if plan == nil {
|
||||
@@ -115,9 +53,6 @@ func stepDependenciesReady(
|
||||
merrors.InvalidArgument("payment plan: step is required")
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// DependsOn — это ПРОСТО готовность, не успех
|
||||
// ------------------------------------------------------------
|
||||
for _, dep := range step.DependsOn {
|
||||
key := strings.TrimSpace(dep)
|
||||
if key == "" {
|
||||
@@ -126,18 +61,18 @@ func stepDependenciesReady(
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -203,11 +138,11 @@ func stepDependenciesReady(
|
||||
}
|
||||
|
||||
if execStep.IsTerminal() {
|
||||
// завершился не фейлом → блокируем
|
||||
// complete with fail, block
|
||||
return false, false, true, nil
|
||||
}
|
||||
|
||||
// ещё выполняется → ждём
|
||||
// still exexuting, wait
|
||||
return false, true, false, nil
|
||||
}
|
||||
|
||||
@@ -243,42 +178,6 @@ func stepDependenciesReady(
|
||||
}
|
||||
}
|
||||
|
||||
func commitAfterDependenciesSucceeded(step *model.PaymentStep, execSteps map[string]*model.ExecutionStep) bool {
|
||||
if step == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
commitAfter := step.CommitAfter
|
||||
if len(commitAfter) == 0 {
|
||||
commitAfter = step.DependsOn
|
||||
}
|
||||
if len(commitAfter) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, dep := range commitAfter {
|
||||
key := strings.TrimSpace(dep)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
execStep := execSteps[key]
|
||||
if execStep == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch execStep.State {
|
||||
case model.OperationStateSuccess,
|
||||
model.OperationStateSkipped:
|
||||
continue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func cardPayoutDependenciesConfirmed(
|
||||
plan *model.PaymentPlan,
|
||||
execPlan *model.ExecutionPlan,
|
||||
|
||||
@@ -38,8 +38,8 @@ func (p *paymentExecutor) executePlanStep(
|
||||
|
||||
logger.Debug("Executing payment plan step")
|
||||
|
||||
if isStepFinal(execStep) {
|
||||
logger.Debug("Step already in final state, skipping execution",
|
||||
if execStep.IsTerminal() {
|
||||
logger.Debug("Step already in terminal state, skipping execution",
|
||||
zap.String("state", string(execStep.State)),
|
||||
)
|
||||
return false, nil
|
||||
@@ -282,18 +282,21 @@ func (p *paymentExecutor) executeSendStep(
|
||||
case model.RailProviderSettlement:
|
||||
logger.Debug("Preparing provider settlement transfer")
|
||||
|
||||
amount, err := requireMoney(cloneMoney(payment.Intent.Amount), "provider settlement amount")
|
||||
amount, err := requireMoney(cloneMoney(payment.LastQuote.DebitSettlementAmount), "provider settlement amount")
|
||||
if err != nil {
|
||||
logger.Warn("Invalid provider settlement amount", zap.Error(err))
|
||||
logger.Warn("Invalid provider settlement amount", zap.Error(err), zap.Any("settlement", payment.LastQuote.DebitSettlementAmount))
|
||||
return false, err
|
||||
}
|
||||
logger.Debug("Expected settlement amount", zap.String("amount", amount.Amount), zap.String("currency", amount.Currency))
|
||||
fee, err := requireMoney(cloneMoney(payment.LastQuote.ExpectedFeeTotal), "provider settlement amount")
|
||||
if err != nil {
|
||||
logger.Warn("Invalid provider settlement amount", zap.Error(err))
|
||||
logger.Warn("Invalid fee settlement amount", zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
if fee.Currency != amount.Currency {
|
||||
logger.Warn("Fee and amount currencies do not match")
|
||||
logger.Warn("Fee and amount currencies do not match",
|
||||
zap.String("amount_currency", amount.Currency), zap.String("fee_currency", fee.Currency),
|
||||
)
|
||||
return false, merrors.DataConflict("settlement payment: currencies mismatch")
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
|
||||
|
||||
quote := &orchestratorv1.PaymentQuote{
|
||||
DebitAmount: debitAmount,
|
||||
DebitSettlementAmount: payAmount,
|
||||
ExpectedSettlementAmount: settlementAmount,
|
||||
ExpectedFeeTotal: feeTotal,
|
||||
FeeLines: feeLines,
|
||||
|
||||
@@ -253,6 +253,7 @@ type Customer struct {
|
||||
// PaymentQuoteSnapshot stores the latest quote info.
|
||||
type PaymentQuoteSnapshot struct {
|
||||
DebitAmount *paymenttypes.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"`
|
||||
DebitSettlementAmount *paymenttypes.Money `bson:"debitSettlementAmount,omitempty" json:"debitSettlementAmount,omitempty"`
|
||||
ExpectedSettlementAmount *paymenttypes.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"`
|
||||
ExpectedFeeTotal *paymenttypes.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"`
|
||||
FeeLines []*paymenttypes.FeeLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"`
|
||||
@@ -326,6 +327,10 @@ func (s *ExecutionStep) IsSuccess() bool {
|
||||
return s.State == OperationStateSuccess
|
||||
}
|
||||
|
||||
func (s *ExecutionStep) IsFailed() bool {
|
||||
return s.State == OperationStateFailed
|
||||
}
|
||||
|
||||
func (s *ExecutionStep) ReadyForNext() bool {
|
||||
switch s.State {
|
||||
case OperationStateSuccess,
|
||||
|
||||
Reference in New Issue
Block a user