outbox for gateways

This commit is contained in:
Stephan D
2026-02-18 01:35:28 +01:00
parent 974caf286c
commit 69531cee73
221 changed files with 12172 additions and 782 deletions

View File

@@ -64,5 +64,5 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
)

View File

@@ -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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -49,7 +49,7 @@ func stepLiveness(
pStep, ok := pStepIdx[step.Code]
if !ok {
logger.Error("step missing in payment plan",
logger.Error("Step missing in payment plan",
zap.String("step_id", step.Code),
)
return StepDead
@@ -58,7 +58,7 @@ func stepLiveness(
for _, depID := range pStep.DependsOn {
dep := eStepIdx[depID]
if dep == nil {
logger.Warn("dependency missing in execution plan",
logger.Warn("Dependency missing in execution plan",
zap.String("step_id", step.Code),
zap.String("dep_id", depID),
)

View File

@@ -71,7 +71,7 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
}
chainClient, _, err := s.resolveChainGatewayClient(ctx, network, intentAmount, actions, instanceID, payment.PaymentRef)
if err != nil {
s.logger.Warn("card funding gateway resolution failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
s.logger.Warn("Card funding gateway resolution failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
return err
}
@@ -116,7 +116,7 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
EstimatedTotalFee: estimatedTotalFee,
})
if err != nil {
s.logger.Warn("card funding gas top-up compute failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
s.logger.Warn("Card funding gas top-up compute failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
return err
}
if computeResp != nil {
@@ -211,7 +211,7 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
PaymentRef: payment.PaymentRef,
})
if gasErr != nil {
s.logger.Warn("card gas top-up transfer failed", zap.Error(gasErr), zap.String("payment_ref", payment.PaymentRef))
s.logger.Warn("Card gas top-up transfer failed", zap.Error(gasErr), zap.String("payment_ref", payment.PaymentRef))
return gasErr
}
if gasStep != nil {
@@ -252,7 +252,7 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
}
}
if gasStep != nil {
s.logger.Info("card gas top-up transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", gasStep.TransferRef))
s.logger.Info("Card gas top-up transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", gasStep.TransferRef))
}
updateExecutionPlanTotalNetworkFee(plan)
}
@@ -300,7 +300,7 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
feeStep.TransferRef = exec.FeeTransferRef
}
setExecutionStepStatus(feeStep, model.OperationStateWaiting)
s.logger.Info("card fee transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", exec.FeeTransferRef))
s.logger.Info("Card fee transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", exec.FeeTransferRef))
}
payment.Execution = exec
@@ -325,16 +325,16 @@ func (s *Service) estimateTransferNetworkFee(ctx context.Context, client chaincl
Amount: cloneProtoMoney(amount),
})
if err != nil {
s.logger.Warn("chain gateway fee estimation failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
s.logger.Warn("Chain gateway fee estimation failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
}
if resp == nil {
s.logger.Warn("chain gateway fee estimation returned empty response", zap.String("source_wallet_ref", sourceWalletRef))
s.logger.Warn("Chain gateway fee estimation returned empty response", zap.String("source_wallet_ref", sourceWalletRef))
return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
}
fee := resp.GetNetworkFee()
if fee == nil || strings.TrimSpace(fee.GetAmount()) == "" || strings.TrimSpace(fee.GetCurrency()) == "" {
s.logger.Warn("chain gateway fee estimation missing network fee", zap.String("source_wallet_ref", sourceWalletRef))
s.logger.Warn("Chain gateway fee estimation missing network fee", zap.String("source_wallet_ref", sourceWalletRef))
return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
}
return cloneProtoMoney(fee), nil

View File

@@ -9,7 +9,7 @@ import (
func (s *Service) cardRoute(gateway string) (CardGatewayRoute, error) {
if len(s.deps.cardRoutes) == 0 {
s.logger.Warn("card routing not configured", zap.String("gateway", gateway))
s.logger.Warn("Card routing not configured", zap.String("gateway", gateway))
return CardGatewayRoute{}, merrors.InvalidArgument("card routing not configured")
}
key := strings.ToLower(strings.TrimSpace(gateway))
@@ -18,11 +18,11 @@ func (s *Service) cardRoute(gateway string) (CardGatewayRoute, error) {
}
route, ok := s.deps.cardRoutes[key]
if !ok {
s.logger.Warn("card routing missing for gateway", zap.String("gateway", key))
s.logger.Warn("Card routing missing for gateway", zap.String("gateway", key))
return CardGatewayRoute{}, merrors.InvalidArgument("card routing missing for gateway " + key)
}
if strings.TrimSpace(route.FundingAddress) == "" {
s.logger.Warn("card routing missing funding address", zap.String("gateway", key))
s.logger.Warn("Card routing missing funding address", zap.String("gateway", key))
return CardGatewayRoute{}, merrors.InvalidArgument("card funding address is required for gateway " + key)
}
return route, nil

View File

@@ -108,7 +108,7 @@ func (s *Service) submitCardPayout(ctx context.Context, operationRef string, pay
}
resp, err := s.deps.mntx.client.CreateCardTokenPayout(ctx, req)
if err != nil {
s.logger.Warn("card token payout failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
s.logger.Warn("Card token payout failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
return err
}
state = resp.GetPayout()
@@ -138,7 +138,7 @@ func (s *Service) submitCardPayout(ctx context.Context, operationRef string, pay
}
resp, err := s.deps.mntx.client.CreateCardPayout(ctx, req)
if err != nil {
s.logger.Warn("card payout failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
s.logger.Warn("Card payout failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
return err
}
state = resp.GetPayout()
@@ -175,7 +175,7 @@ func (s *Service) submitCardPayout(ctx context.Context, operationRef string, pay
updateExecutionPlanTotalNetworkFee(plan)
}
s.logger.Info("card payout submitted", zap.String("payment_ref", payment.PaymentRef),
s.logger.Info("Card payout submitted", zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_id", exec.CardPayoutRef), zap.String("operation_ref", state.OperationRef))
return nil

View File

@@ -147,17 +147,17 @@ func updateExecutionStepsFromGatewayExecution(
zap.String("gateway_status", string(exec.Status)),
)
log.Debug("gateway execution received")
log.Debug("Gateway execution received")
if payment == nil || payment.PaymentPlan == nil || exec == nil {
log.Warn("invalid input: payment/plan/exec is nil")
log.Warn("Invalid input: payment/plan/exec is nil")
return paymodel.PaymentStateSubmitted,
merrors.DataConflict("payment is missing plan or execution step")
}
operationRef := strings.TrimSpace(exec.OperationRef)
if operationRef == "" {
log.Warn("empty operation_ref from gateway")
log.Warn("Empty operation_ref from gateway")
return paymodel.PaymentStateSubmitted,
merrors.InvalidArgument("no operation reference provided")
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/payments/orchestrator/internal/service/shared"
"github.com/tech/sendico/payments/storage"
"github.com/tech/sendico/payments/storage/model"
quotestorage "github.com/tech/sendico/payments/storage/quote"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
@@ -50,7 +51,7 @@ func (h *initiatePaymentsCommand) Execute(ctx context.Context, req *orchestrator
}
record, err := quotesStore.GetByRef(ctx, orgRef, quoteRef)
if err != nil {
if errors.Is(err, storage.ErrQuoteNotFound) {
if errors.Is(err, quotestorage.ErrQuoteNotFound) {
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, "quote_not_found", merrors.InvalidArgument("quote_ref not found or expired"))
}
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)

View File

@@ -126,12 +126,12 @@ func (h *paymentEventHandler) processTransferUpdate(ctx context.Context, req *or
if payment.Execution.CardPayoutRef == "" {
payment.State = model.PaymentStateFundsReserved
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))
} else if err := h.submitCardPayout(ctx, transfer.GetOperationRef(), payment); err != nil {
payment.State = model.PaymentStateFailed
payment.FailureCode = model.PaymentFailureCodePolicy
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))
}
}
}
@@ -143,7 +143,7 @@ func (h *paymentEventHandler) processTransferUpdate(ctx context.Context, req *or
if err := store.Update(ctx, payment); err != nil {
return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
}
h.logger.Info("transfer update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", transferRef), zap.Any("state", payment.State))
h.logger.Info("Transfer update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", transferRef), zap.Any("state", payment.State))
return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
}
@@ -151,7 +151,7 @@ func (h *paymentEventHandler) processTransferUpdate(ctx context.Context, req *or
if err := store.Update(ctx, payment); err != nil {
return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
}
h.logger.Info("transfer update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", transferRef), zap.Any("state", payment.State))
h.logger.Info("Transfer update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", transferRef), zap.Any("state", payment.State))
return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
}
@@ -198,7 +198,7 @@ func (h *paymentEventHandler) processDepositObserved(ctx context.Context, req *o
if err := store.Update(ctx, payment); err != nil {
return gsresponse.Auto[orchestratorv1.ProcessDepositObservedResponse](h.logger, mservice.PaymentOrchestrator, err)
}
h.logger.Info("deposit observed matched payment", zap.String("payment_ref", payment.PaymentRef), zap.String("wallet_ref", walletRef))
h.logger.Info("Deposit observed matched payment", zap.String("payment_ref", payment.PaymentRef), zap.String("wallet_ref", walletRef))
return gsresponse.Success(&orchestratorv1.ProcessDepositObservedResponse{Payment: toProtoPayment(payment)})
}
return gsresponse.Success(&orchestratorv1.ProcessDepositObservedResponse{})
@@ -231,7 +231,7 @@ func (h *paymentEventHandler) processCardPayoutUpdate(ctx context.Context, req *
switch payout.GetStatus() {
case mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS:
h.logger.Info("card payout success received",
h.logger.Info("Card payout success received",
zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_ref", payout.GetPayoutId()),
zap.String("payment_state_before", string(payment.State)),
@@ -241,19 +241,19 @@ func (h *paymentEventHandler) processCardPayoutUpdate(ctx context.Context, req *
if h.resumePlan != nil && payment.PaymentPlan != nil && len(payment.PaymentPlan.Steps) > 0 {
if err := h.resumePlan(ctx, store, payment); err != nil {
h.logger.Error("resumePlan failed after payout success",
h.logger.Error("ResumePlan failed after payout success",
zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_ref", payout.GetPayoutId()),
zap.Error(err),
)
return gsresponse.Auto[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
}
h.logger.Info("resumePlan executed after payout success",
h.logger.Info("ResumePlan executed after payout success",
zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_ref", payout.GetPayoutId()),
)
} else {
h.logger.Warn("payout success but plan cannot be resumed",
h.logger.Warn("Payout success but plan cannot be resumed",
zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_ref", payout.GetPayoutId()),
zap.Bool("resume_plan_present", h.resumePlan != nil),
@@ -262,7 +262,7 @@ func (h *paymentEventHandler) processCardPayoutUpdate(ctx context.Context, req *
}
case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
h.logger.Warn("card payout failed",
h.logger.Warn("Card payout failed",
zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_ref", payout.GetPayoutId()),
zap.String("provider_message", payout.GetProviderMessage()),
@@ -273,13 +273,13 @@ func (h *paymentEventHandler) processCardPayoutUpdate(ctx context.Context, req *
payment.FailureReason = strings.TrimSpace(payout.GetProviderMessage())
if h.releaseHold != nil && payment.PaymentPlan != nil && len(payment.PaymentPlan.Steps) > 0 {
h.logger.Info("releasing hold after payout failure",
h.logger.Info("Releasing hold after payout failure",
zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_ref", payout.GetPayoutId()),
)
if err := h.releaseHold(ctx, store, payment); err != nil {
h.logger.Error("releaseHold failed after payout failure",
h.logger.Error("ReleaseHold failed after payout failure",
zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_ref", payout.GetPayoutId()),
zap.Error(err),
@@ -287,7 +287,7 @@ func (h *paymentEventHandler) processCardPayoutUpdate(ctx context.Context, req *
return gsresponse.Auto[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
}
} else {
h.logger.Warn("payout failed but hold cannot be released",
h.logger.Warn("Payout failed but hold cannot be released",
zap.String("payment_ref", payment.PaymentRef),
zap.String("payout_ref", payout.GetPayoutId()),
zap.Bool("release_hold_present", h.releaseHold != nil),
@@ -300,7 +300,7 @@ func (h *paymentEventHandler) processCardPayoutUpdate(ctx context.Context, req *
return gsresponse.Auto[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
}
h.logger.Info("card payout update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("payout_id", paymentRef), zap.Any("state", payment.State))
h.logger.Info("Card payout update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("payout_id", paymentRef), zap.Any("state", payment.State))
return gsresponse.Success(&orchestratorv1.ProcessCardPayoutUpdateResponse{
Payment: toProtoPayment(payment),
})

View File

@@ -47,7 +47,7 @@ func (h *paymentQueryHandler) getPayment(ctx context.Context, req *orchestratorv
if err != nil {
return paymentNotFoundResponder[orchestratorv1.GetPaymentResponse](mservice.PaymentOrchestrator, h.logger, err)
}
h.logger.Debug("payment fetched", zap.String("payment_ref", paymentRef))
h.logger.Debug("Payment fetched", zap.String("payment_ref", paymentRef))
return gsresponse.Success(&orchestratorv1.GetPaymentResponse{Payment: toProtoPayment(entity)})
}
@@ -76,6 +76,6 @@ func (h *paymentQueryHandler) listPayments(ctx context.Context, req *orchestrato
for _, item := range result.Items {
resp.Payments = append(resp.Payments, toProtoPayment(item))
}
h.logger.Debug("payments listed", zap.Int("count", len(resp.Payments)), zap.String("next_cursor", resp.GetPage().GetNextCursor()))
h.logger.Debug("Payments listed", zap.Int("count", len(resp.Payments)), zap.String("next_cursor", resp.GetPage().GetNextCursor()))
return gsresponse.Success(resp)
}

View File

@@ -116,7 +116,7 @@ func (s *Service) resolvePaymentQuote(ctx context.Context, in quoteResolutionInp
}
record, err := quotesStore.GetByRef(ctx, in.OrgID, ref)
if err != nil {
if errors.Is(err, storage.ErrQuoteNotFound) {
if errors.Is(err, quotestorage.ErrQuoteNotFound) {
return nil, nil, nil, quoteResolutionError{code: "quote_not_found", err: merrors.InvalidArgument("quote_ref not found or expired")}
}
return nil, nil, nil, err

View File

@@ -477,17 +477,17 @@ func (s *helperQuotesStore) Create(_ context.Context, _ *model.PaymentQuoteRecor
func (s *helperQuotesStore) GetByRef(_ context.Context, _ bson.ObjectID, ref string) (*model.PaymentQuoteRecord, error) {
if s.records == nil {
return nil, storage.ErrQuoteNotFound
return nil, quotestorage.ErrQuoteNotFound
}
if rec, ok := s.records[ref]; ok {
return rec, nil
}
return nil, storage.ErrQuoteNotFound
return nil, quotestorage.ErrQuoteNotFound
}
func (s *helperQuotesStore) GetByIdempotencyKey(_ context.Context, orgRef bson.ObjectID, ref string) (*model.PaymentQuoteRecord, error) {
if s.records == nil {
return nil, storage.ErrQuoteNotFound
return nil, quotestorage.ErrQuoteNotFound
}
for _, rec := range s.records {
if rec.OrganizationRef != orgRef {
@@ -497,7 +497,7 @@ func (s *helperQuotesStore) GetByIdempotencyKey(_ context.Context, orgRef bson.O
return rec, nil
}
}
return nil, storage.ErrQuoteNotFound
return nil, quotestorage.ErrQuoteNotFound
}
type helperQuotationClient struct {

View File

@@ -447,17 +447,17 @@ func (s *stubQuotesStore) Create(ctx context.Context, quote *model.PaymentQuoteR
func (s *stubQuotesStore) GetByRef(ctx context.Context, orgRef bson.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error) {
if s.quotes == nil {
return nil, storage.ErrQuoteNotFound
return nil, quotestorage.ErrQuoteNotFound
}
if q, ok := s.quotes[strings.TrimSpace(quoteRef)]; ok {
return q, nil
}
return nil, storage.ErrQuoteNotFound
return nil, quotestorage.ErrQuoteNotFound
}
func (s *stubQuotesStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.PaymentQuoteRecord, error) {
if s.quotes == nil {
return nil, storage.ErrQuoteNotFound
return nil, quotestorage.ErrQuoteNotFound
}
for _, q := range s.quotes {
if q.OrganizationRef != orgRef {
@@ -467,7 +467,7 @@ func (s *stubQuotesStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.O
return q, nil
}
}
return nil, storage.ErrQuoteNotFound
return nil, quotestorage.ErrQuoteNotFound
}
type stubRoutesStore struct {

View File

@@ -47,7 +47,7 @@ func (b *defaultPlanBuilder) Build(ctx context.Context, payment *model.Payment,
logger.Warn("Failed to build fx conversion plan", zap.Error(err))
return nil, err
}
logger.Info("fx conversion plan built", zap.Int("steps", len(plan.Steps)))
logger.Info("Fx conversion plan built", zap.Int("steps", len(plan.Steps)))
return plan, nil
}

View File

@@ -104,7 +104,7 @@ func (b *defaultPlanBuilder) buildPlanFromTemplate(ctx context.Context, payment
action, err := actionForOperation(tpl.Operation)
if err != nil {
b.logger.Warn("plan builder: unsupported operation in plan template step", zap.String("step_id", stepID), zap.String("operation", tpl.Operation), zap.Error(err))
b.logger.Warn("Plan builder: unsupported operation in plan template step", zap.String("step_id", stepID), zap.String("operation", tpl.Operation), zap.Error(err))
return nil, err
}