fixed status orchestration

This commit is contained in:
Stephan D
2026-02-05 15:33:41 +01:00
parent 7417b33de3
commit 5df02baa80
6 changed files with 85 additions and 68 deletions

View File

@@ -73,7 +73,8 @@ func SubmitTransferNative(
if contract != "" { if contract != "" {
normalizedContract, err := normalizeAddress(contract) normalizedContract, err := normalizeAddress(contract)
if err != nil { if err != nil {
logger.Warn("Invalid TRON contract address", zap.String("contract_address", contract), zap.String("transfer_ref", transfer.TransferRef), zap.Error(err)) logger.Warn("Invalid TRON contract address", zap.String("transfer_ref", transfer.TransferRef),
zap.String("contract_address", contract), zap.Error(err))
return "", err return "", err
} }
contract = normalizedContract contract = normalizedContract

View File

@@ -314,8 +314,6 @@ func applyCardPayoutUpdate(payment *model.Payment, payout *mntxv1.CardPayoutStat
} }
payment.State = mapMntxStatusToState(payout.GetStatus())
switch payout.GetStatus() { switch payout.GetStatus() {
case mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS: case mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS:

View File

@@ -2,6 +2,7 @@ package orchestrator
import ( import (
"context" "context"
"fmt"
"strings" "strings"
paymodel "github.com/tech/sendico/payments/orchestrator/storage/model" paymodel "github.com/tech/sendico/payments/orchestrator/storage/model"
@@ -80,6 +81,7 @@ func (s *Service) onGatewayExecution(ctx context.Context, exec *model.PaymentGat
payment, err := store.GetByPaymentRef(ctx, paymentRef) payment, err := store.GetByPaymentRef(ctx, paymentRef)
if err != nil { if err != nil {
s.logger.Warn("Failed to fetch payment from database", zap.Error(err))
return err return err
} }
@@ -92,8 +94,8 @@ func (s *Service) onGatewayExecution(ctx context.Context, exec *model.PaymentGat
payment.Metadata["gateway_request_idempotency"] = exec.IdempotencyKey payment.Metadata["gateway_request_idempotency"] = exec.IdempotencyKey
// --- update exactly ONE step // --- update exactly ONE step
updated := updateExecutionStepsFromGatewayExecution(s.logger, payment, exec)
if !updated { if payment.State, err = updateExecutionStepsFromGatewayExecution(s.logger, payment, exec); err != nil {
s.logger.Warn("No execution step matched gateway result", s.logger.Warn("No execution step matched gateway result",
zap.String("payment_ref", paymentRef), zap.String("payment_ref", paymentRef),
zap.String("operation_ref", exec.OperationRef), zap.String("operation_ref", exec.OperationRef),
@@ -137,45 +139,43 @@ func updateExecutionStepsFromGatewayExecution(
logger mlogger.Logger, logger mlogger.Logger,
payment *paymodel.Payment, payment *paymodel.Payment,
exec *model.PaymentGatewayExecution, exec *model.PaymentGatewayExecution,
) bool { ) (paymodel.PaymentState, error) {
log := logger.With(
zap.String("payment_ref", payment.PaymentRef),
zap.String("operation_ref", strings.TrimSpace(exec.OperationRef)),
zap.String("gateway_status", string(exec.Status)),
)
log.Debug("gateway execution received")
if payment == nil || payment.PaymentPlan == nil || exec == nil { if payment == nil || payment.PaymentPlan == nil || exec == nil {
logger.Warn("updateExecutionSteps: invalid input", log.Warn("invalid input: payment/plan/exec is nil")
zap.String("payment_ref", payment.PaymentRef), return paymodel.PaymentStateSubmitted,
) merrors.DataConflict("payment is missing plan or execution step")
return false
} }
operationRef := strings.TrimSpace(exec.OperationRef) operationRef := strings.TrimSpace(exec.OperationRef)
if operationRef == "" { if operationRef == "" {
logger.Warn("updateExecutionSteps: empty operation_ref from gateway", log.Warn("empty operation_ref from gateway")
zap.String("payment_ref", payment.PaymentRef), return paymodel.PaymentStateSubmitted,
) merrors.InvalidArgument("no operation reference provided")
return false
} }
execPlan := ensureExecutionPlanForPlan(payment, payment.PaymentPlan) execPlan := ensureExecutionPlanForPlan(payment, payment.PaymentPlan)
if execPlan == nil { if execPlan == nil {
logger.Error("updateExecutionSteps: execution plan missing", log.Warn("Execution plan missing")
zap.String("payment_ref", payment.PaymentRef), return paymodel.PaymentStateSubmitted, merrors.InvalidArgument("execution plan missing")
)
return false
} }
status := executionStepStatusFromGatewayStatus(exec.Status) status := executionStepStatusFromGatewayStatus(exec.Status)
if status == "" { if status == "" {
logger.Warn("updateExecutionSteps: unknown gateway status", log.Warn("Unknown gateway status")
zap.String("payment_ref", payment.PaymentRef), return paymodel.PaymentStateSubmitted,
zap.String("gateway_status", string(exec.Status)), merrors.DataConflict(fmt.Sprintf("unknown gateway status: %s", exec.Status))
)
return false
} }
logger.Debug("updateExecutionSteps: matching by operation_ref", var matched bool
zap.String("payment_ref", payment.PaymentRef),
zap.String("operation_ref", operationRef),
zap.String("mapped_status", string(status)),
)
for idx, execStep := range execPlan.Steps { for idx, execStep := range execPlan.Steps {
if execStep == nil { if execStep == nil {
@@ -184,37 +184,83 @@ func updateExecutionStepsFromGatewayExecution(
if strings.EqualFold(strings.TrimSpace(execStep.OperationRef), operationRef) { if strings.EqualFold(strings.TrimSpace(execStep.OperationRef), operationRef) {
logger.Debug("updateExecutionSteps: matched execution step", log.Debug("Execution step matched",
zap.String("payment_ref", payment.PaymentRef),
zap.Int("step_index", idx), zap.Int("step_index", idx),
zap.String("step_code", execStep.Code), zap.String("step_code", execStep.Code),
zap.String("prev_state", string(execStep.State)), zap.String("prev_state", string(execStep.State)),
) )
// update transfer ref if not set yet
if execStep.TransferRef == "" && exec.TransferRef != "" { if execStep.TransferRef == "" && exec.TransferRef != "" {
execStep.TransferRef = strings.TrimSpace(exec.TransferRef) execStep.TransferRef = strings.TrimSpace(exec.TransferRef)
log.Debug("Transfer_ref attached to step", zap.String("transfer_ref", execStep.TransferRef))
} }
setExecutionStepStatus(execStep, status) setExecutionStepStatus(execStep, status)
logger.Debug("updateExecutionSteps: step state updated", log.Debug("Execution step state updated",
zap.String("payment_ref", payment.PaymentRef),
zap.Int("step_index", idx), zap.Int("step_index", idx),
zap.String("step_code", execStep.Code), zap.String("step_code", execStep.Code),
zap.String("new_state", string(execStep.State)), zap.String("new_state", string(execStep.State)),
) )
return true matched = true
break
} }
} }
logger.Error("updateExecutionSteps: no execution step found for operation_ref", if !matched {
zap.String("payment_ref", payment.PaymentRef), log.Warn("No execution step found for operation_ref")
zap.String("operation_ref", operationRef), return paymodel.PaymentStateSubmitted,
merrors.InvalidArgument(
fmt.Sprintf("execution step not found for operation reference: %s", operationRef),
)
}
// -------- GLOBAL REDUCTION --------
var (
hasSuccess bool
allDone = true
) )
return false for idx, step := range execPlan.Steps {
if step == nil {
continue
}
log.Debug("Evaluating step for payment state",
zap.Int("step_index", idx),
zap.String("step_code", step.Code),
zap.String("step_state", string(step.State)),
)
switch step.State {
case paymodel.OperationStateFailed:
payment.FailureReason = step.Error
log.Info("Payment marked as FAILED due to step failure",
zap.String("failed_step_code", step.Code),
zap.String("error", step.Error),
)
return paymodel.PaymentStateFailed, nil
case paymodel.OperationStateSuccess:
hasSuccess = true
case paymodel.OperationStateSkipped:
// ok
default:
allDone = false
}
}
if hasSuccess && allDone {
log.Info("Payment marked as SUCCESS (all steps completed)")
return paymodel.PaymentStateSuccess, nil
}
log.Info("Payment still PROCESSING (steps not finished)")
return paymodel.PaymentStateSubmitted, nil
} }
func executionStepStatusFromGatewayStatus(status rail.OperationResult) paymodel.OperationState { func executionStepStatusFromGatewayStatus(status rail.OperationResult) paymodel.OperationState {

View File

@@ -90,20 +90,9 @@ func (h *paymentEventHandler) processTransferUpdate(ctx context.Context, req *or
if err := h.resumePlan(ctx, store, payment); err != nil { if err := h.resumePlan(ctx, store, payment); err != nil {
return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err) return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
} }
} else {
payment.State = model.PaymentStateSubmitted
if err := store.Update(ctx, payment); err != nil {
return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
}
} }
return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)}) return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
case chainv1.TransferStatus_TRANSFER_WAITING: case chainv1.TransferStatus_TRANSFER_WAITING:
if payment.State != model.PaymentStateFailed && payment.State != model.PaymentStateCancelled && payment.State != model.PaymentStateSettled {
payment.State = model.PaymentStateSubmitted
}
if err := store.Update(ctx, payment); err != nil {
return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
}
return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)}) return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
default: default:
if err := store.Update(ctx, payment); err != nil { if err := store.Update(ctx, payment); err != nil {
@@ -134,9 +123,7 @@ func (h *paymentEventHandler) processTransferUpdate(ctx context.Context, req *or
case chainv1.TransferStatus_TRANSFER_SUCCESS: case chainv1.TransferStatus_TRANSFER_SUCCESS:
if payment.State != model.PaymentStateFailed && payment.State != model.PaymentStateCancelled && payment.State != model.PaymentStateSettled { if payment.State != model.PaymentStateFailed && payment.State != model.PaymentStateCancelled && payment.State != model.PaymentStateSettled {
if cardPayoutDependenciesConfirmed(payment.PaymentPlan, payment.ExecutionPlan) { if cardPayoutDependenciesConfirmed(payment.PaymentPlan, payment.ExecutionPlan) {
if payment.Execution.CardPayoutRef != "" { if payment.Execution.CardPayoutRef == "" {
payment.State = model.PaymentStateSubmitted
} else {
payment.State = model.PaymentStateFundsReserved payment.State = model.PaymentStateFundsReserved
if h.submitCardPayout == nil { if h.submitCardPayout == nil {
h.logger.Warn("card payout execution skipped", zap.String("payment_ref", payment.PaymentRef)) h.logger.Warn("card payout execution skipped", zap.String("payment_ref", payment.PaymentRef))
@@ -145,18 +132,11 @@ func (h *paymentEventHandler) processTransferUpdate(ctx context.Context, req *or
payment.FailureCode = model.PaymentFailureCodePolicy payment.FailureCode = model.PaymentFailureCodePolicy
payment.FailureReason = strings.TrimSpace(err.Error()) payment.FailureReason = strings.TrimSpace(err.Error())
h.logger.Warn("card payout execution failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef)) h.logger.Warn("card payout execution failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
} else {
payment.State = model.PaymentStateSubmitted
} }
} }
} else {
payment.State = model.PaymentStateSubmitted
} }
} }
case chainv1.TransferStatus_TRANSFER_WAITING: case chainv1.TransferStatus_TRANSFER_WAITING:
if payment.State != model.PaymentStateFailed && payment.State != model.PaymentStateCancelled && payment.State != model.PaymentStateSettled {
payment.State = model.PaymentStateSubmitted
}
default: default:
// keep current state // keep current state
} }

View File

@@ -251,15 +251,6 @@ func (p *paymentExecutor) executePaymentPlan(
return nil return nil
} }
if payment.State != model.PaymentStateSubmitted &&
payment.State != model.PaymentStateFundsReserved {
payment.State = model.PaymentStateSubmitted
if err := store.Update(ctx, payment); err != nil {
return err
}
}
if payment.ExecutionPlan == nil { if payment.ExecutionPlan == nil {
logger.Debug("Initializing execution plan from payment plan") logger.Debug("Initializing execution plan from payment plan")
payment.ExecutionPlan = ensureExecutionPlanForPlan(payment, payment.PaymentPlan) payment.ExecutionPlan = ensureExecutionPlanForPlan(payment, payment.PaymentPlan)

View File

@@ -49,6 +49,7 @@ const (
PaymentStateAccepted PaymentState = "accepted" PaymentStateAccepted PaymentState = "accepted"
PaymentStateFundsReserved PaymentState = "funds_reserved" PaymentStateFundsReserved PaymentState = "funds_reserved"
PaymentStateSubmitted PaymentState = "submitted" PaymentStateSubmitted PaymentState = "submitted"
PaymentStateSuccess PaymentState = "success"
PaymentStateSettled PaymentState = "settled" PaymentStateSettled PaymentState = "settled"
PaymentStateFailed PaymentState = "failed" PaymentStateFailed PaymentState = "failed"
PaymentStateCancelled PaymentState = "cancelled" PaymentStateCancelled PaymentState = "cancelled"