Fully separated payment quotation and orchestration flows
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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
|
||||
}
|
||||
123
api/payments/orchestrator/internal/service/execution/export.go
Normal file
123
api/payments/orchestrator/internal/service/execution/export.go
Normal file
@@ -0,0 +1,123 @@
|
||||
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"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/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 *orchestratorv1.PaymentQuote,
|
||||
quoteFromSnapshot func(*model.PaymentQuoteSnapshot) *orchestratorv1.PaymentQuote,
|
||||
) *orchestratorv1.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)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/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 *orchestratorv1.PaymentQuote,
|
||||
quoteFromSnapshot func(*model.PaymentQuoteSnapshot) *orchestratorv1.PaymentQuote,
|
||||
) *orchestratorv1.PaymentQuote {
|
||||
if quote != nil {
|
||||
return quote
|
||||
}
|
||||
if payment != nil && payment.LastQuote != nil && quoteFromSnapshot != nil {
|
||||
return quoteFromSnapshot(payment.LastQuote)
|
||||
}
|
||||
return &orchestratorv1.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 != model.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 != model.RailLedger || step.Action != model.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 != model.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 model.RailLedger:
|
||||
if step.Action == model.RailOperationFXConvert {
|
||||
return model.PaymentFailureCodeFX
|
||||
}
|
||||
return model.PaymentFailureCodeLedger
|
||||
case model.RailCrypto:
|
||||
return model.PaymentFailureCodeChain
|
||||
default:
|
||||
return model.PaymentFailureCodePolicy
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"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 != model.RailCardPayout ||
|
||||
step.Action != model.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
|
||||
}
|
||||
Reference in New Issue
Block a user