improved logging in callbacks

This commit is contained in:
Stephan D
2026-03-03 01:07:35 +01:00
parent b10ec79fe0
commit bae4cd6e35
45 changed files with 226 additions and 146 deletions

View File

@@ -21,6 +21,7 @@ import (
msg "github.com/tech/sendico/pkg/messaging" msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
@@ -385,7 +386,7 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID
logFields := []zap.Field{zap.Time("booked_at_used", bookedAt)} logFields := []zap.Field{zap.Time("booked_at_used", bookedAt)}
if !orgRef.IsZero() { if !orgRef.IsZero() {
logFields = append(logFields, zap.String("organization_ref", orgRef.Hex())) logFields = append(logFields, mzap.ObjRef("organization_ref", orgRef))
} }
logFields = append(logFields, logFieldsFromIntent(intent)...) logFields = append(logFields, logFieldsFromIntent(intent)...)

View File

@@ -73,9 +73,10 @@ func validateQuoteIdempotency(previewOnly bool, idempotencyKey string) error {
} }
type InitiatePayment struct { type InitiatePayment struct {
PaymentBase `json:",inline"` PaymentBase `json:",inline"`
Intent *PaymentIntent `json:"intent,omitempty"` Intent *PaymentIntent `json:"intent,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"` QuoteRef string `json:"quoteRef,omitempty"`
ClientPaymentRef string `json:"clientPaymentRef,omitempty"`
} }
func (r InitiatePayment) Validate() error { func (r InitiatePayment) Validate() error {
@@ -106,8 +107,9 @@ func (r InitiatePayment) Validate() error {
} }
type InitiatePayments struct { type InitiatePayments struct {
PaymentBase `json:",inline"` PaymentBase `json:",inline"`
QuoteRef string `json:"quoteRef,omitempty"` QuoteRef string `json:"quoteRef,omitempty"`
ClientPaymentRef string `json:"clientPaymentRef,omitempty"`
} }
func (r *InitiatePayments) Validate() error { func (r *InitiatePayments) Validate() error {

View File

@@ -120,11 +120,11 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
var user model.Account var user model.Account
err = a.db.Get(ctx, accountRef, &user) err = a.db.Get(ctx, accountRef, &user)
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
a.logger.Info("User not found for password reset", zap.String("account_ref", accountRef.Hex())) a.logger.Info("User not found for password reset", mzap.ObjRef("account_ref", accountRef))
return response.NotFound(a.logger, a.Name(), "User not found") return response.NotFound(a.logger, a.Name(), "User not found")
} }
if err != nil { if err != nil {
a.logger.Warn("Failed to get user for password reset", zap.Error(err), zap.String("account_ref", accountRef.Hex())) a.logger.Warn("Failed to get user for password reset", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return response.Auto(a.logger, a.Name(), err) return response.Auto(a.logger, a.Name(), err)
} }
@@ -140,7 +140,7 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
} }
if t.AccountRef != accountRef { if t.AccountRef != accountRef {
a.logger.Warn("Token account reference does not match request account reference", zap.String("token_account_ref", t.AccountRef.Hex()), zap.String("request_account_ref", accountRef.Hex())) a.logger.Warn("Token account reference does not match request account reference", mzap.ObjRef("token_account_ref", t.AccountRef), mzap.ObjRef("request_account_ref", accountRef))
return response.DataConflict(a.logger, a.Name(), "Token does not match account") return response.DataConflict(a.logger, a.Name(), "Token does not match account")
} }

View File

@@ -13,6 +13,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/pkg/vault/kv" "github.com/tech/sendico/pkg/vault/kv"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
@@ -152,7 +153,7 @@ func (m *vaultSigningSecretManager) Provision(
} }
secretRef := "vault:" + secretPath + "#" + m.field secretRef := "vault:" + secretPath + "#" + m.field
m.logger.Info("Callback signing secret stored", zap.String("secret_ref", secretRef), zap.String("callback_ref", callbackRef.Hex())) m.logger.Info("Callback signing secret stored", zap.String("secret_ref", secretRef), mzap.ObjRef("callback_ref", callbackRef))
return secretRef, secret, nil return secretRef, secret, nil
} }

View File

@@ -11,6 +11,7 @@ import (
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/model/account_role" "github.com/tech/sendico/pkg/model/account_role"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1" describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/srequest"
@@ -88,7 +89,7 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
Describable: describable, Describable: describable,
}) })
if err != nil { if err != nil {
a.logger.Warn("Failed to create ledger account", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to create ledger account", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return response.Auto(a.logger, mservice.Ledger, err) return response.Auto(a.logger, mservice.Ledger, err)
} }

View File

@@ -47,7 +47,7 @@ func (a *LedgerAPI) listAccounts(r *http.Request, account *model.Account, token
resp, err := a.client.ListAccounts(ctx, req) resp, err := a.client.ListAccounts(ctx, req)
if err != nil { if err != nil {
a.logger.Warn("Failed to list ledger accounts", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to list ledger accounts", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return response.Auto(a.logger, mservice.Ledger, err) return response.Auto(a.logger, mservice.Ledger, err)
} }

View File

@@ -89,7 +89,7 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
req := &orchestrationv2.ExecutePaymentRequest{ req := &orchestrationv2.ExecutePaymentRequest{
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey), Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
QuotationRef: quotationRef, QuotationRef: quotationRef,
ClientPaymentRef: metadataValue(payload.Metadata, "client_payment_ref"), ClientPaymentRef: strings.TrimSpace(payload.ClientPaymentRef),
} }
resp, err := a.execution.ExecutePayment(ctx, req) resp, err := a.execution.ExecutePayment(ctx, req)
@@ -110,6 +110,7 @@ func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
} }
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey) payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef) payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
payload.ClientPaymentRef = strings.TrimSpace(payload.ClientPaymentRef)
if err := payload.Validate(); err != nil { if err := payload.Validate(); err != nil {
return nil, err return nil, err

View File

@@ -14,12 +14,12 @@ import (
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
) )
func TestInitiateByQuote_DoesNotUseIntentRef(t *testing.T) { func TestInitiateByQuote_ForwardsClientPaymentRef(t *testing.T) {
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{} exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec) api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","metadata":{"client_payment_ref":"client-ref-1"}}` body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","clientPaymentRef":"client-ref-1"}`
rr := invokeInitiateByQuote(t, api, orgRef, body) rr := invokeInitiateByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want { if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String()) t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
@@ -32,6 +32,24 @@ func TestInitiateByQuote_DoesNotUseIntentRef(t *testing.T) {
} }
} }
func TestInitiateByQuote_DoesNotForwardLegacyClientPaymentRefFromMetadata(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","metadata":{"client_payment_ref":"legacy-client-ref"}}`
rr := invokeInitiateByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got, want := len(exec.executeReqs), 1; got != want {
t.Fatalf("execute calls mismatch: got=%d want=%d", got, want)
}
if got := exec.executeReqs[0].GetClientPaymentRef(); got != "" {
t.Fatalf("expected empty client_payment_ref, got=%q", got)
}
}
func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) { func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) {
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{} exec := &fakeExecutionClientForBatch{}

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
"github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/interface/api/sresponse"
@@ -39,7 +40,7 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
return response.BadPayload(a.logger, a.Name(), err) return response.BadPayload(a.logger, a.Name(), err)
} }
clientPaymentRef := metadataValue(payload.Metadata, "client_payment_ref") clientPaymentRef := strings.TrimSpace(payload.ClientPaymentRef)
idempotencyKey := strings.TrimSpace(payload.IdempotencyKey) idempotencyKey := strings.TrimSpace(payload.IdempotencyKey)
quotationRef := strings.TrimSpace(payload.QuoteRef) quotationRef := strings.TrimSpace(payload.QuoteRef)
@@ -50,7 +51,7 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
} }
resp, err := a.execution.ExecuteBatchPayment(ctx, req) resp, err := a.execution.ExecuteBatchPayment(ctx, req)
if err != nil { if err != nil {
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to initiate batch payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return grpcErrorResponse(a.logger, a.Name(), err) return grpcErrorResponse(a.logger, a.Name(), err)
} }
@@ -72,6 +73,7 @@ func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments,
} }
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey) payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef) payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
payload.ClientPaymentRef = strings.TrimSpace(payload.ClientPaymentRef)
if err := payload.Validate(); err != nil { if err := payload.Validate(); err != nil {
return nil, err return nil, err

View File

@@ -50,7 +50,7 @@ func TestInitiatePaymentsByQuote_ForwardsClientPaymentRef(t *testing.T) {
exec := &fakeExecutionClientForBatch{} exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec) api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","metadata":{"client_payment_ref":"client-ref-1"}}` body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","clientPaymentRef":"client-ref-1"}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body) rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want { if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String()) t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
@@ -67,6 +67,25 @@ func TestInitiatePaymentsByQuote_ForwardsClientPaymentRef(t *testing.T) {
} }
} }
func TestInitiatePaymentsByQuote_DoesNotForwardLegacyClientPaymentRefFromMetadata(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","metadata":{"client_payment_ref":"legacy-client-ref"}}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got, want := len(exec.executeBatchReqs), 1; got != want {
t.Fatalf("execute batch calls mismatch: got=%d want=%d", got, want)
}
if got := exec.executeBatchReqs[0].GetClientPaymentRef(); got != "" {
t.Fatalf("expected empty client_payment_ref, got=%q", got)
}
}
func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefField(t *testing.T) { func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefField(t *testing.T) {
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{} exec := &fakeExecutionClientForBatch{}

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2" quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
"github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/interface/api/sresponse"
@@ -61,7 +62,7 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
resp, err := a.quotation.QuotePayment(ctx, req) resp, err := a.quotation.QuotePayment(ctx, req)
if err != nil { if err != nil {
a.logger.Warn("Failed to quote payment", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to quote payment", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return grpcErrorResponse(a.logger, a.Name(), err) return grpcErrorResponse(a.logger, a.Name(), err)
} }
@@ -117,7 +118,7 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
resp, err := a.quotation.QuotePayments(ctx, req) resp, err := a.quotation.QuotePayments(ctx, req)
if err != nil { if err != nil {
a.logger.Warn("Failed to quote payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to quote payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return grpcErrorResponse(a.logger, a.Name(), err) return grpcErrorResponse(a.logger, a.Name(), err)
} }

View File

@@ -12,6 +12,7 @@ import (
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
"github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param" mutil "github.com/tech/sendico/server/internal/mutil/param"
@@ -65,24 +66,24 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
return response.Auto(a.logger, a.Name(), merrors.NoData("no crypto gateways available")) return response.Auto(a.logger, a.Name(), merrors.NoData("no crypto gateways available"))
} }
a.logger.Debug("Resolved CRYPTO gateways for wallet balance lookup", a.logger.Debug("Resolved CRYPTO gateways for wallet balance lookup",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.Int("gateway_count", len(cryptoGateways))) zap.Int("gateway_count", len(cryptoGateways)))
route, routeErr := a.walletRoute(ctx, orgRef.Hex(), walletRef) route, routeErr := a.walletRoute(ctx, orgRef.Hex(), walletRef)
if routeErr != nil { if routeErr != nil {
a.logger.Warn("Failed to resolve wallet route", zap.Error(routeErr), zap.String("wallet_ref", walletRef), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to resolve wallet route", zap.Error(routeErr), zap.String("wallet_ref", walletRef), mzap.ObjRef("organization_ref", orgRef))
} }
if route != nil { if route != nil {
a.logger.Debug("Resolved stored wallet route", a.logger.Debug("Resolved stored wallet route",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("route_network", route.Network), zap.String("route_network", route.Network),
zap.String("route_gateway_id", route.GatewayID)) zap.String("route_gateway_id", route.GatewayID))
preferred := findGatewayForRoute(cryptoGateways, route) preferred := findGatewayForRoute(cryptoGateways, route)
if preferred != nil { if preferred != nil {
a.logger.Debug("Using preferred gateway from stored wallet route", a.logger.Debug("Using preferred gateway from stored wallet route",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("gateway_id", preferred.ID), zap.String("gateway_id", preferred.ID),
zap.String("network", preferred.Network), zap.String("network", preferred.Network),
@@ -91,7 +92,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
if preferredErr == nil && bal != nil { if preferredErr == nil && bal != nil {
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, preferred.Network, preferred.ID) a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, preferred.Network, preferred.ID)
a.logger.Debug("Wallet balance resolved via preferred gateway", a.logger.Debug("Wallet balance resolved via preferred gateway",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("gateway_id", preferred.ID), zap.String("gateway_id", preferred.ID),
zap.String("network", preferred.Network)) zap.String("network", preferred.Network))
@@ -124,20 +125,20 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
} }
} else { } else {
a.logger.Warn("Stored wallet route did not match any healthy discovery gateway", a.logger.Warn("Stored wallet route did not match any healthy discovery gateway",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("route_network", route.Network), zap.String("route_network", route.Network),
zap.String("route_gateway_id", route.GatewayID)) zap.String("route_gateway_id", route.GatewayID))
} }
} else { } else {
a.logger.Debug("Stored wallet route not found; using gateway fallback", a.logger.Debug("Stored wallet route not found; using gateway fallback",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef)) zap.String("wallet_ref", walletRef))
} }
// Fall back to querying remaining gateways in parallel. // Fall back to querying remaining gateways in parallel.
a.logger.Debug("Starting fallback wallet balance fan-out", a.logger.Debug("Starting fallback wallet balance fan-out",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.Int("gateway_count", len(cryptoGateways))) zap.Int("gateway_count", len(cryptoGateways)))
bal, err := a.queryBalanceFromGateways(ctx, cryptoGateways, orgRef.Hex(), walletRef) bal, err := a.queryBalanceFromGateways(ctx, cryptoGateways, orgRef.Hex(), walletRef)

View File

@@ -81,7 +81,7 @@ func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresp
return response.Auto(a.logger, a.Name(), merrors.InvalidArgument("no gateway available for network: "+networkName)) return response.Auto(a.logger, a.Name(), merrors.InvalidArgument("no gateway available for network: "+networkName))
} }
a.logger.Debug("Selected gateway for wallet creation", a.logger.Debug("Selected gateway for wallet creation",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("network", networkName), zap.String("network", networkName),
zap.String("gateway_id", gateway.ID), zap.String("gateway_id", gateway.ID),
zap.String("gateway_network", gateway.Network), zap.String("gateway_network", gateway.Network),
@@ -134,7 +134,7 @@ func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresp
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, networkName, gateway.ID) a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, networkName, gateway.ID)
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, gateway.Network, gateway.ID) a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, gateway.Network, gateway.ID)
a.logger.Debug("Persisted wallet route after wallet creation", a.logger.Debug("Persisted wallet route after wallet creation",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("network", networkName), zap.String("network", networkName),
zap.String("gateway_id", gateway.ID)) zap.String("gateway_id", gateway.ID))

View File

@@ -59,7 +59,7 @@ func (a *WalletAPI) listWallets(r *http.Request, account *model.Account, token *
return sresponse.Wallets(a.logger, nil, token) return sresponse.Wallets(a.logger, nil, token)
} }
a.logger.Debug("Resolved CRYPTO gateways for wallet list", a.logger.Debug("Resolved CRYPTO gateways for wallet list",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.Int("gateway_count", len(cryptoGateways))) zap.Int("gateway_count", len(cryptoGateways)))
// Build request // Build request
@@ -80,7 +80,7 @@ func (a *WalletAPI) listWallets(r *http.Request, account *model.Account, token *
allAccounts := a.queryAllGateways(ctx, cryptoGateways, req) allAccounts := a.queryAllGateways(ctx, cryptoGateways, req)
dedupedAccounts := dedupeAccountsByWalletRef(allAccounts) dedupedAccounts := dedupeAccountsByWalletRef(allAccounts)
a.logger.Debug("Wallet list fan-out completed", a.logger.Debug("Wallet list fan-out completed",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.Int("accounts_raw", len(allAccounts)), zap.Int("accounts_raw", len(allAccounts)),
zap.Int("accounts_deduped", len(dedupedAccounts)), zap.Int("accounts_deduped", len(dedupedAccounts)),
zap.Int("gateway_count", len(cryptoGateways))) zap.Int("gateway_count", len(cryptoGateways)))

View File

@@ -15,6 +15,7 @@ import (
"github.com/tech/sendico/edge/callbacks/internal/storage" "github.com/tech/sendico/edge/callbacks/internal/storage"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -171,14 +172,14 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
result = "blocked" result = "blocked"
s.logger.Warn("Blocked task delivery due to URL validation failure", s.logger.Warn("Blocked task delivery due to URL validation failure",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.String("event_id", task.EventID), zap.String("event_id", task.EventID),
zap.Error(err), zap.Error(err),
) )
if markErr := s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC()); markErr != nil { if markErr := s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC()); markErr != nil {
s.logger.Warn("Failed to mark blocked task as failed", s.logger.Warn("Failed to mark blocked task as failed",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.Error(markErr), zap.Error(markErr),
) )
} }
@@ -195,7 +196,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
result = "sign_error" result = "sign_error"
s.logger.Warn("Failed to sign task payload", s.logger.Warn("Failed to sign task payload",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.String("event_id", task.EventID), zap.String("event_id", task.EventID),
zap.String("signing_mode", task.SigningMode), zap.String("signing_mode", task.SigningMode),
zap.Error(err), zap.Error(err),
@@ -203,7 +204,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
if markErr := s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC()); markErr != nil { if markErr := s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC()); markErr != nil {
s.logger.Warn("Failed to mark signing-error task as failed", s.logger.Warn("Failed to mark signing-error task as failed",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.Error(markErr), zap.Error(markErr),
) )
} }
@@ -218,7 +219,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
result = "request_error" result = "request_error"
s.logger.Warn("Failed to build callback request", s.logger.Warn("Failed to build callback request",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.String("event_id", task.EventID), zap.String("event_id", task.EventID),
zap.String("endpoint_url", task.EndpointURL), zap.String("endpoint_url", task.EndpointURL),
zap.Error(err), zap.Error(err),
@@ -226,7 +227,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
if markErr := s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC()); markErr != nil { if markErr := s.tasks.MarkFailed(ctx, task.ID, attempt, err.Error(), statusCode, time.Now().UTC()); markErr != nil {
s.logger.Warn("Failed to mark request-error task as failed", s.logger.Warn("Failed to mark request-error task as failed",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.Error(markErr), zap.Error(markErr),
) )
} }
@@ -253,7 +254,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
case outcomeDelivered: case outcomeDelivered:
result = string(outcomeDelivered) result = string(outcomeDelivered)
if err := s.tasks.MarkDelivered(ctx, task.ID, statusCode, time.Since(started), now); err != nil { if err := s.tasks.MarkDelivered(ctx, task.ID, statusCode, time.Since(started), now); err != nil {
s.logger.Warn("Failed to mark task delivered", zap.String("worker_id", workerID), zap.String("task_id", task.ID.Hex()), zap.Error(err)) s.logger.Warn("Failed to mark task delivered", zap.String("worker_id", workerID), mzap.ObjRef("task_ref", task.ID), zap.Error(err))
} }
case outcomeRetry: case outcomeRetry:
if attempt < task.MaxAttempts { if attempt < task.MaxAttempts {
@@ -265,7 +266,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
} }
s.logger.Warn("Task delivery retry scheduled", s.logger.Warn("Task delivery retry scheduled",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.String("event_id", task.EventID), zap.String("event_id", task.EventID),
zap.Int("attempt", attempt), zap.Int("attempt", attempt),
zap.Int("status_code", statusCode), zap.Int("status_code", statusCode),
@@ -273,7 +274,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
zap.Time("next_attempt_at", next), zap.Time("next_attempt_at", next),
) )
if err := s.tasks.MarkRetry(ctx, task.ID, attempt, next, lastErr, statusCode, now); err != nil { if err := s.tasks.MarkRetry(ctx, task.ID, attempt, next, lastErr, statusCode, now); err != nil {
s.logger.Warn("Failed to mark task retry", zap.String("worker_id", workerID), zap.String("task_id", task.ID.Hex()), zap.Error(err)) s.logger.Warn("Failed to mark task retry", zap.String("worker_id", workerID), mzap.ObjRef("task_ref", task.ID), zap.Error(err))
} }
} else { } else {
result = string(outcomeFailed) result = string(outcomeFailed)
@@ -283,7 +284,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
} }
s.logger.Warn("Task delivery failed after reaching max attempts", s.logger.Warn("Task delivery failed after reaching max attempts",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.String("event_id", task.EventID), zap.String("event_id", task.EventID),
zap.Int("attempt", attempt), zap.Int("attempt", attempt),
zap.Int("max_attempts", task.MaxAttempts), zap.Int("max_attempts", task.MaxAttempts),
@@ -291,7 +292,7 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
zap.String("reason", lastErr), zap.String("reason", lastErr),
) )
if err := s.tasks.MarkFailed(ctx, task.ID, attempt, lastErr, statusCode, now); err != nil { if err := s.tasks.MarkFailed(ctx, task.ID, attempt, lastErr, statusCode, now); err != nil {
s.logger.Warn("Failed to mark task failed", zap.String("worker_id", workerID), zap.String("task_id", task.ID.Hex()), zap.Error(err)) s.logger.Warn("Failed to mark task failed", zap.String("worker_id", workerID), mzap.ObjRef("task_ref", task.ID), zap.Error(err))
} }
} }
default: default:
@@ -302,14 +303,14 @@ func (s *service) handleTask(ctx context.Context, workerID string, task *model.T
} }
s.logger.Warn("Task delivery failed", s.logger.Warn("Task delivery failed",
zap.String("worker_id", workerID), zap.String("worker_id", workerID),
zap.String("task_id", task.ID.Hex()), mzap.ObjRef("task_ref", task.ID),
zap.String("event_id", task.EventID), zap.String("event_id", task.EventID),
zap.Int("attempt", attempt), zap.Int("attempt", attempt),
zap.Int("status_code", statusCode), zap.Int("status_code", statusCode),
zap.String("reason", lastErr), zap.String("reason", lastErr),
) )
if err := s.tasks.MarkFailed(ctx, task.ID, attempt, lastErr, statusCode, now); err != nil { if err := s.tasks.MarkFailed(ctx, task.ID, attempt, lastErr, statusCode, now); err != nil {
s.logger.Warn("Failed to mark task failed", zap.String("worker_id", workerID), zap.String("task_id", task.ID.Hex()), zap.Error(err)) s.logger.Warn("Failed to mark task failed", zap.String("worker_id", workerID), mzap.ObjRef("task_ref", task.ID), zap.Error(err))
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/edge/callbacks/internal/storage" "github.com/tech/sendico/edge/callbacks/internal/storage"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -45,7 +46,7 @@ func (s *service) Resolve(ctx context.Context, eventType string, organizationRef
if err != nil { if err != nil {
s.logger.Warn("Failed to resolve active endpoints", s.logger.Warn("Failed to resolve active endpoints",
zap.String("event_type", eventType), zap.String("event_type", eventType),
zap.String("organization_ref", organizationRef.Hex()), mzap.ObjRef("organization_ref", organizationRef),
zap.Error(err), zap.Error(err),
) )
return nil, err return nil, err
@@ -53,7 +54,7 @@ func (s *service) Resolve(ctx context.Context, eventType string, organizationRef
s.logger.Debug("Resolved active endpoints", s.logger.Debug("Resolved active endpoints",
zap.String("event_type", eventType), zap.String("event_type", eventType),
zap.String("organization_ref", organizationRef.Hex()), mzap.ObjRef("organization_ref", organizationRef),
zap.Int("endpoints", len(endpoints)), zap.Int("endpoints", len(endpoints)),
) )

View File

@@ -66,8 +66,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest(journalEntryTypeCredit) recordDuplicateRequest(journalEntryTypeCredit)
logger.Info("Duplicate credit request (idempotency)", logger.Info("Duplicate credit request (idempotency)", mzap.StorableRef(existingEntry))
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
Version: existingEntry.Version, Version: existingEntry.Version,

View File

@@ -64,8 +64,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest(journalEntryTypeDebit) recordDuplicateRequest(journalEntryTypeDebit)
logger.Info("Duplicate debit request (idempotency)", logger.Info("Duplicate debit request (idempotency)", mzap.StorableRef(existingEntry))
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
Version: existingEntry.Version, Version: existingEntry.Version,
@@ -123,7 +122,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {

View File

@@ -62,7 +62,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest(journalEntryTypeCredit) recordDuplicateRequest(journalEntryTypeCredit)
logger.Info("Duplicate external credit request (idempotency)", logger.Info("Duplicate external credit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex())) mzap.StorableRef(existingEntry))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
Version: existingEntry.Version, Version: existingEntry.Version,
@@ -140,7 +140,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
@@ -287,8 +287,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest(journalEntryTypeDebit) recordDuplicateRequest(journalEntryTypeDebit)
logger.Info("Duplicate external debit request (idempotency)", logger.Info("Duplicate external debit request (idempotency)", mzap.StorableRef(existingEntry))
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
Version: existingEntry.Version, Version: existingEntry.Version,
@@ -366,7 +365,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {

View File

@@ -77,8 +77,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest(journalEntryTypeFX) recordDuplicateRequest(journalEntryTypeFX)
logger.Info("Duplicate FX request (idempotency)", logger.Info("Duplicate FX request (idempotency)", mzap.StorableRef(existingEntry))
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
Version: existingEntry.Version, Version: existingEntry.Version,
@@ -162,7 +161,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("Failed to get FX charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get FX charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {

View File

@@ -139,7 +139,7 @@ func (s *Service) resolveSettlementAccount(ctx context.Context, orgRef bson.Obje
if errors.Is(err, storage.ErrAccountNotFound) { if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData("contra account not found") return nil, merrors.NoData("contra account not found")
} }
s.logger.Warn("Failed to load override contra account", zap.Error(err), zap.String("accountRef", overrideRef.Hex())) s.logger.Warn("Failed to load override contra account", zap.Error(err), mzap.ObjRef("account_ref", overrideRef))
return nil, merrors.Internal("failed to load contra account") return nil, merrors.Internal("failed to load contra account")
} }
if err := validateAccountForOrg(account, orgRef, currency); err != nil { if err := validateAccountForOrg(account, orgRef, currency); err != nil {

View File

@@ -87,8 +87,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest(journalEntryTypeTransfer) recordDuplicateRequest(journalEntryTypeTransfer)
logger.Info("Duplicate transfer request (idempotency)", logger.Info("Duplicate transfer request (idempotency)", mzap.StorableRef(existingEntry))
zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
Version: existingEntry.Version, Version: existingEntry.Version,
@@ -172,7 +171,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {

View File

@@ -83,17 +83,17 @@ func (b *balancesStore) Upsert(ctx context.Context, balance *model.AccountBalanc
if err := b.repo.FindOneByFilter(ctx, filter, existing); err != nil { if err := b.repo.FindOneByFilter(ctx, filter, existing); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
b.logger.Debug("Inserting new balance", zap.String("accountRef", balance.AccountRef.Hex())) b.logger.Debug("Inserting new balance", mzap.ObjRef("account_ref", balance.AccountRef))
return b.repo.Insert(ctx, balance, filter) return b.repo.Insert(ctx, balance, filter)
} }
b.logger.Warn("Failed to fetch balance", zap.Error(err), zap.String("accountRef", balance.AccountRef.Hex())) b.logger.Warn("Failed to fetch balance", zap.Error(err), mzap.ObjRef("account_ref", balance.AccountRef))
return err return err
} }
if existing.GetID() != nil { if existing.GetID() != nil {
balance.SetID(*existing.GetID()) balance.SetID(*existing.GetID())
} }
b.logger.Debug("Updating balance", zap.String("accountRef", balance.AccountRef.Hex()), b.logger.Debug("Updating balance", mzap.ObjRef("account_ref", balance.AccountRef),
zap.String("balance", balance.Balance)) zap.String("balance", balance.Balance))
return b.repo.Update(ctx, balance) return b.repo.Update(ctx, balance)
} }

View File

@@ -11,7 +11,7 @@ import (
const ( const (
paymentTypeAccount pkgmodel.PaymentType = 8 paymentTypeAccount pkgmodel.PaymentType = 8
maxPrivateMethodResolutionDepth = 8 maxPrivateMethodResolutionDepth int = 8
) )
func (s *Service) GetPaymentMethodPrivate(ctx context.Context, req *methodsv1.GetPaymentMethodPrivateRequest) (*methodsv1.GetPaymentMethodPrivateResponse, error) { func (s *Service) GetPaymentMethodPrivate(ctx context.Context, req *methodsv1.GetPaymentMethodPrivateRequest) (*methodsv1.GetPaymentMethodPrivateResponse, error) {

View File

@@ -8,6 +8,7 @@ import (
np "github.com/tech/sendico/pkg/messaging/notifications/processor" np "github.com/tech/sendico/pkg/messaging/notifications/processor"
nm "github.com/tech/sendico/pkg/model/notification" nm "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -62,25 +63,25 @@ func (s *Service) onRecipientNotification(
if err != nil { if err != nil {
s.logger.Warn("Failed to cascade archive payment methods by recipient", s.logger.Warn("Failed to cascade archive payment methods by recipient",
zap.Error(err), zap.Error(err),
zap.String("recipient_ref", recipientRef.Hex()), mzap.ObjRef("recipient_ref", recipientRef),
zap.String("actor_account_ref", actorAccountRef.Hex())) mzap.ObjRef("actor_account_ref", actorAccountRef))
return err return err
} }
s.logger.Info("Recipient archive cascade applied to payment methods", s.logger.Info("Recipient archive cascade applied to payment methods",
zap.String("recipient_ref", recipientRef.Hex()), mzap.ObjRef("recipient_ref", recipientRef),
zap.String("actor_account_ref", actorAccountRef.Hex()), mzap.ObjRef("actor_account_ref", actorAccountRef),
zap.Int("updated_count", updated)) zap.Int("updated_count", updated))
case nm.NADeleted: case nm.NADeleted:
if err := s.pmstore.DeleteByRecipient(ctx, recipientRef); err != nil { if err := s.pmstore.DeleteByRecipient(ctx, recipientRef); err != nil {
s.logger.Warn("Failed to cascade delete payment methods by recipient", s.logger.Warn("Failed to cascade delete payment methods by recipient",
zap.Error(err), zap.Error(err),
zap.String("recipient_ref", recipientRef.Hex()), mzap.ObjRef("recipient_ref", recipientRef),
zap.String("actor_account_ref", actorAccountRef.Hex())) mzap.ObjRef("actor_account_ref", actorAccountRef))
return err return err
} }
s.logger.Info("Recipient delete cascade applied to payment methods", s.logger.Info("Recipient delete cascade applied to payment methods",
zap.String("recipient_ref", recipientRef.Hex()), mzap.ObjRef("recipient_ref", recipientRef),
zap.String("actor_account_ref", actorAccountRef.Hex())) mzap.ObjRef("actor_account_ref", actorAccountRef))
} }
return nil return nil

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
pm "github.com/tech/sendico/pkg/model" pm "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -24,7 +25,7 @@ type svc struct {
func (s *svc) Create(in Input) (payment *Payment, err error) { func (s *svc) Create(in Input) (payment *Payment, err error) {
logger := s.logger logger := s.logger
logger.Debug("Starting Create", logger.Debug("Starting Create",
zap.String("organization_ref", in.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", in.OrganizationRef),
zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)), zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
zap.Int("steps_count", len(in.Steps)), zap.Int("steps_count", len(in.Steps)),
) )

View File

@@ -10,6 +10,7 @@ import (
"github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -26,7 +27,7 @@ func (s *svc) TryReuse(
) (payment *model.Payment, reused bool, err error) { ) (payment *model.Payment, reused bool, err error) {
logger := s.logger logger := s.logger
logger.Debug("Starting Try reuse", logger.Debug("Starting Try reuse",
zap.String("organization_ref", in.OrganizationID.Hex()), mzap.ObjRef("organization_ref", payment.OrganizationRef),
zap.Bool("has_idempotency_key", strings.TrimSpace(in.IdempotencyKey) != ""), zap.Bool("has_idempotency_key", strings.TrimSpace(in.IdempotencyKey) != ""),
) )
defer func(start time.Time) { defer func(start time.Time) {

View File

@@ -12,6 +12,7 @@ import (
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -61,7 +62,7 @@ func newService(deps Dependencies) (Service, error) {
func (s *svc) GetPayment(ctx context.Context, in GetPaymentInput) (payment *agg.Payment, err error) { func (s *svc) GetPayment(ctx context.Context, in GetPaymentInput) (payment *agg.Payment, err error) {
logger := s.logger logger := s.logger
logger.Debug("Starting Get payment", logger.Debug("Starting Get payment",
zap.String("organization_ref", in.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", in.OrganizationRef),
zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)), zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
) )
defer func(start time.Time) { defer func(start time.Time) {
@@ -93,7 +94,7 @@ func (s *svc) GetPayment(ctx context.Context, in GetPaymentInput) (payment *agg.
func (s *svc) ListPayments(ctx context.Context, in ListPaymentsInput) (out *ListPaymentsOutput, err error) { func (s *svc) ListPayments(ctx context.Context, in ListPaymentsInput) (out *ListPaymentsOutput, err error) {
logger := s.logger logger := s.logger
logger.Debug("Starting List payments", logger.Debug("Starting List payments",
zap.String("organization_ref", in.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", in.OrganizationRef),
zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)), zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
zap.Int("states_count", len(in.States)), zap.Int("states_count", len(in.States)),
zap.Int32("limit", in.Limit), zap.Int32("limit", in.Limit),

View File

@@ -175,13 +175,13 @@ func (s *svc) GetByPaymentRef(ctx context.Context, orgRef bson.ObjectID, payment
logger := s.logger logger := s.logger
requestPaymentRef := strings.TrimSpace(paymentRef) requestPaymentRef := strings.TrimSpace(paymentRef)
logger.Debug("Starting Get by payment ref", logger.Debug("Starting Get by payment ref",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("payment_ref", requestPaymentRef), zap.String("payment_ref", requestPaymentRef),
) )
defer func(start time.Time) { defer func(start time.Time) {
fields := []zap.Field{ fields := []zap.Field{
zap.Int64("duration_ms", time.Since(start).Milliseconds()), zap.Int64("duration_ms", time.Since(start).Milliseconds()),
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("payment_ref", requestPaymentRef), zap.String("payment_ref", requestPaymentRef),
} }
if payment != nil { if payment != nil {
@@ -225,7 +225,7 @@ func (s *svc) GetByPaymentRefGlobal(ctx context.Context, paymentRef string) (pay
} }
if payment != nil { if payment != nil {
fields = append(fields, fields = append(fields,
zap.String("organization_ref", payment.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", payment.OrganizationRef),
zap.String("state", string(payment.State)), zap.String("state", string(payment.State)),
zap.Uint64("version", payment.Version), zap.Uint64("version", payment.Version),
) )
@@ -253,7 +253,7 @@ func (s *svc) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, ide
logger := s.logger logger := s.logger
hasKey := strings.TrimSpace(idempotencyKey) != "" hasKey := strings.TrimSpace(idempotencyKey) != ""
logger.Debug("Starting Get by idempotency key", logger.Debug("Starting Get by idempotency key",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.Bool("has_idempotency_key", hasKey), zap.Bool("has_idempotency_key", hasKey),
) )
defer func(start time.Time) { defer func(start time.Time) {
@@ -298,7 +298,7 @@ func (s *svc) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, ide
func (s *svc) ListByQuotationRef(ctx context.Context, in ListByQuotationRefInput) (out *ListOutput, err error) { func (s *svc) ListByQuotationRef(ctx context.Context, in ListByQuotationRefInput) (out *ListOutput, err error) {
logger := s.logger logger := s.logger
logger.Debug("Starting List by quotation ref", logger.Debug("Starting List by quotation ref",
zap.String("organization_ref", in.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", in.OrganizationRef),
zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)), zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
zap.Int32("limit", in.Limit), zap.Int32("limit", in.Limit),
) )
@@ -337,7 +337,7 @@ func (s *svc) ListByQuotationRef(ctx context.Context, in ListByQuotationRefInput
func (s *svc) ListByState(ctx context.Context, in ListByStateInput) (out *ListOutput, err error) { func (s *svc) ListByState(ctx context.Context, in ListByStateInput) (out *ListOutput, err error) {
logger := s.logger logger := s.logger
logger.Debug("Starting List by state", logger.Debug("Starting List by state",
zap.String("organization_ref", in.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", in.OrganizationRef),
zap.String("state", string(in.State)), zap.String("state", string(in.State)),
zap.Int32("limit", in.Limit), zap.Int32("limit", in.Limit),
) )

View File

@@ -5,12 +5,13 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"github.com/tech/sendico/pkg/discovery"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/idem" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/idem"
@@ -60,8 +61,8 @@ func (s *svc) ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.Exec
} }
resolved, err := s.quote.ResolveAll(ctx, s.quoteStore, qsnap.ResolveAllInput{ resolved, err := s.quote.ResolveAll(ctx, s.quoteStore, qsnap.ResolveAllInput{
OrganizationID: requestCtx.OrganizationID, OrganizationRef: requestCtx.OrganizationID,
QuotationRef: requestCtx.QuotationRef, QuotationRef: requestCtx.QuotationRef,
}) })
if err != nil { if err != nil {
return nil, remapResolveError(err) return nil, remapResolveError(err)

View File

@@ -11,6 +11,7 @@ import (
pon "github.com/tech/sendico/pkg/messaging/notifications/paymentorchestrator" pon "github.com/tech/sendico/pkg/messaging/notifications/paymentorchestrator"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -64,7 +65,7 @@ func (p *brokerPaymentStatusPublisher) Publish(_ context.Context, in paymentStat
if paymentRef == "" || payment.OrganizationRef.IsZero() { if paymentRef == "" || payment.OrganizationRef.IsZero() {
p.logger.Warn("Skipping payment status publish due to missing identifiers", p.logger.Warn("Skipping payment status publish due to missing identifiers",
zap.String("payment_ref", paymentRef), zap.String("payment_ref", paymentRef),
zap.String("organization_ref", payment.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", payment.OrganizationRef),
) )
return nil return nil
} }

View File

@@ -36,8 +36,8 @@ type Output struct {
// ResolveAllInput defines lookup scope for resolving all items in a batch quotation. // ResolveAllInput defines lookup scope for resolving all items in a batch quotation.
type ResolveAllInput struct { type ResolveAllInput struct {
OrganizationID bson.ObjectID OrganizationRef bson.ObjectID
QuotationRef string QuotationRef string
} }
// ResolveAllOutput contains all resolved items from a batch quotation. // ResolveAllOutput contains all resolved items from a batch quotation.

View File

@@ -41,8 +41,8 @@ func TestResolveAll_BatchReturnsAllItems(t *testing.T) {
return record, nil return record, nil
}, },
}, ResolveAllInput{ }, ResolveAllInput{
OrganizationID: orgID, OrganizationRef: orgID,
QuotationRef: "batch-quote-ref", QuotationRef: "batch-quote-ref",
}) })
if err != nil { if err != nil {
t.Fatalf("ResolveAll returned error: %v", err) t.Fatalf("ResolveAll returned error: %v", err)
@@ -94,8 +94,8 @@ func TestResolveAll_SingleShapeReturnsOneItem(t *testing.T) {
return record, nil return record, nil
}, },
}, ResolveAllInput{ }, ResolveAllInput{
OrganizationID: orgID, OrganizationRef: orgID,
QuotationRef: "single-quote-ref", QuotationRef: "single-quote-ref",
}) })
if err != nil { if err != nil {
t.Fatalf("ResolveAll returned error: %v", err) t.Fatalf("ResolveAll returned error: %v", err)
@@ -137,8 +137,8 @@ func TestResolveAll_NonExecutableItemFails(t *testing.T) {
return record, nil return record, nil
}, },
}, ResolveAllInput{ }, ResolveAllInput{
OrganizationID: orgID, OrganizationRef: orgID,
QuotationRef: "batch-mixed", QuotationRef: "batch-mixed",
}) })
if err == nil { if err == nil {
t.Fatal("expected error for non-executable item") t.Fatal("expected error for non-executable item")
@@ -172,8 +172,8 @@ func TestResolveAll_ExpiredQuoteFails(t *testing.T) {
return record, nil return record, nil
}, },
}, ResolveAllInput{ }, ResolveAllInput{
OrganizationID: orgID, OrganizationRef: orgID,
QuotationRef: "expired-quote", QuotationRef: "expired-quote",
}) })
if err == nil { if err == nil {
t.Fatal("expected error for expired quote") t.Fatal("expected error for expired quote")
@@ -187,8 +187,8 @@ func TestResolveAll_EmptyQuotationRefFails(t *testing.T) {
resolver := New(Dependencies{Logger: zap.NewNop()}) resolver := New(Dependencies{Logger: zap.NewNop()})
_, err := resolver.ResolveAll(context.Background(), &fakeStore{}, ResolveAllInput{ _, err := resolver.ResolveAll(context.Background(), &fakeStore{}, ResolveAllInput{
OrganizationID: bson.NewObjectID(), OrganizationRef: bson.NewObjectID(),
QuotationRef: "", QuotationRef: "",
}) })
if err == nil { if err == nil {
t.Fatal("expected error for empty quotation_ref") t.Fatal("expected error for empty quotation_ref")
@@ -199,8 +199,8 @@ func TestResolveAll_QuoteNotFoundFails(t *testing.T) {
resolver := New(Dependencies{Logger: zap.NewNop()}) resolver := New(Dependencies{Logger: zap.NewNop()})
_, err := resolver.ResolveAll(context.Background(), &fakeStore{}, ResolveAllInput{ _, err := resolver.ResolveAll(context.Background(), &fakeStore{}, ResolveAllInput{
OrganizationID: bson.NewObjectID(), OrganizationRef: bson.NewObjectID(),
QuotationRef: "nonexistent", QuotationRef: "nonexistent",
}) })
if err == nil { if err == nil {
t.Fatal("expected error for not-found quote") t.Fatal("expected error for not-found quote")
@@ -234,8 +234,8 @@ func TestResolveAll_SetsQuoteRefWhenEmpty(t *testing.T) {
return record, nil return record, nil
}, },
}, ResolveAllInput{ }, ResolveAllInput{
OrganizationID: orgID, OrganizationRef: orgID,
QuotationRef: "batch-ref", QuotationRef: "batch-ref",
}) })
if err != nil { if err != nil {
t.Fatalf("ResolveAll returned error: %v", err) t.Fatalf("ResolveAll returned error: %v", err)

View File

@@ -11,6 +11,7 @@ import (
quotestorage "github.com/tech/sendico/payments/storage/quote" quotestorage "github.com/tech/sendico/payments/storage/quote"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -33,7 +34,7 @@ func (s *svc) Resolve(
) (out *Output, err error) { ) (out *Output, err error) {
logger := s.logger logger := s.logger
logger.Debug("Starting Resolve", logger.Debug("Starting Resolve",
zap.String("organization_ref", in.OrganizationID.Hex()), mzap.ObjRef("organization_ref", in.OrganizationID),
zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)), zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
) )
defer func(start time.Time) { defer func(start time.Time) {
@@ -105,7 +106,7 @@ func (s *svc) ResolveAll(
) (out *ResolveAllOutput, err error) { ) (out *ResolveAllOutput, err error) {
logger := s.logger logger := s.logger
logger.Debug("Starting ResolveAll", logger.Debug("Starting ResolveAll",
zap.String("organization_ref", in.OrganizationID.Hex()), mzap.ObjRef("organization_ref", in.OrganizationRef),
zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)), zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
) )
defer func(start time.Time) { defer func(start time.Time) {
@@ -126,7 +127,7 @@ func (s *svc) ResolveAll(
if store == nil { if store == nil {
return nil, merrors.InvalidArgument("quotes store is required") return nil, merrors.InvalidArgument("quotes store is required")
} }
if in.OrganizationID.IsZero() { if in.OrganizationRef.IsZero() {
return nil, merrors.InvalidArgument("organization_id is required") return nil, merrors.InvalidArgument("organization_id is required")
} }
quoteRef := strings.TrimSpace(in.QuotationRef) quoteRef := strings.TrimSpace(in.QuotationRef)
@@ -134,7 +135,7 @@ func (s *svc) ResolveAll(
return nil, merrors.InvalidArgument("quotation_ref is required") return nil, merrors.InvalidArgument("quotation_ref is required")
} }
record, err := store.GetByRef(ctx, in.OrganizationID, quoteRef) record, err := store.GetByRef(ctx, in.OrganizationRef, quoteRef)
if err != nil { if err != nil {
if errors.Is(err, quotestorage.ErrQuoteNotFound) || errors.Is(err, merrors.ErrNoData) { if errors.Is(err, quotestorage.ErrQuoteNotFound) || errors.Is(err, merrors.ErrNoData) {
return nil, ErrQuoteNotFound return nil, ErrQuoteNotFound

View File

@@ -3,10 +3,12 @@ package orchestrator
import ( import (
"context" "context"
"errors" "errors"
"github.com/tech/sendico/pkg/discovery"
"strings" "strings"
"time" "time"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon" "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/prepo"
@@ -111,7 +113,7 @@ func (s *Service) onPaymentGatewayExecution(ctx context.Context, msg *pmodel.Pay
s.logger.Debug("Reconciling payment from gateway execution event", s.logger.Debug("Reconciling payment from gateway execution event",
zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)), zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
zap.String("organization_ref", payment.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", payment.OrganizationRef),
zap.String("step_ref", strings.TrimSpace(event.StepRef)), zap.String("step_ref", strings.TrimSpace(event.StepRef)),
zap.String("status", strings.TrimSpace(string(event.Status))), zap.String("status", strings.TrimSpace(string(event.Status))),
zap.String("transfer_ref", strings.TrimSpace(event.TransferRef)), zap.String("transfer_ref", strings.TrimSpace(event.TransferRef)),
@@ -457,7 +459,7 @@ func (s *Service) pollObserveCandidate(ctx context.Context, payment *agg.Payment
s.logger.Debug("Reconciling payment from observe polling result", s.logger.Debug("Reconciling payment from observe polling result",
zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)), zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
zap.String("organization_ref", payment.OrganizationRef.Hex()), mzap.ObjRef("organization_ref", payment.OrganizationRef),
zap.String("step_ref", candidate.stepRef), zap.String("step_ref", candidate.stepRef),
zap.String("status", strings.TrimSpace(string(event.Status))), zap.String("status", strings.TrimSpace(string(event.Status))),
zap.String("transfer_ref", candidate.transferRef), zap.String("transfer_ref", candidate.transferRef),

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1" endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2" quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
@@ -33,7 +34,7 @@ type quoteIntentLogSummary struct {
func (s *QuotationServiceV2) quotePaymentLogger(req *quotationv2.QuotePaymentRequest) mlogger.Logger { func (s *QuotationServiceV2) quotePaymentLogger(req *quotationv2.QuotePaymentRequest) mlogger.Logger {
return s.logger.With( return s.logger.With(
zap.String("flow_ref", bson.NewObjectID().Hex()), mzap.ObjRef("flow_ref", bson.NewObjectID()),
zap.String("rpc_method", "QuotePayment"), zap.String("rpc_method", "QuotePayment"),
zap.String("organization_ref", strings.TrimSpace(req.GetMeta().GetOrganizationRef())), zap.String("organization_ref", strings.TrimSpace(req.GetMeta().GetOrganizationRef())),
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())), zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
@@ -44,7 +45,7 @@ func (s *QuotationServiceV2) quotePaymentLogger(req *quotationv2.QuotePaymentReq
func (s *QuotationServiceV2) quotePaymentsLogger(req *quotationv2.QuotePaymentsRequest) mlogger.Logger { func (s *QuotationServiceV2) quotePaymentsLogger(req *quotationv2.QuotePaymentsRequest) mlogger.Logger {
return s.logger.With( return s.logger.With(
zap.String("flow_ref", bson.NewObjectID().Hex()), mzap.ObjRef("flow_ref", bson.NewObjectID()),
zap.String("rpc_method", "QuotePayments"), zap.String("rpc_method", "QuotePayments"),
zap.String("organization_ref", strings.TrimSpace(req.GetMeta().GetOrganizationRef())), zap.String("organization_ref", strings.TrimSpace(req.GetMeta().GetOrganizationRef())),
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())), zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -43,7 +44,7 @@ func (db *IndexableDB[T]) Reorder(ctx context.Context, objectRef bson.ObjectID,
if err != nil { if err != nil {
db.logger.Error("Failed to get object for reordering", db.logger.Error("Failed to get object for reordering",
zap.Error(err), zap.Error(err),
zap.String("object_ref", objectRef.Hex()), mzap.ObjRef("object_ref", objectRef),
zap.Int("new_index", newIndex)) zap.Int("new_index", newIndex))
return err return err
} }
@@ -53,7 +54,7 @@ func (db *IndexableDB[T]) Reorder(ctx context.Context, objectRef bson.ObjectID,
currentIndex := indexable.Index currentIndex := indexable.Index
if currentIndex == newIndex { if currentIndex == newIndex {
db.logger.Debug("No reordering needed - same index", db.logger.Debug("No reordering needed - same index",
zap.String("object_ref", objectRef.Hex()), mzap.ObjRef("object_ref", objectRef),
zap.Int("current_index", currentIndex), zap.Int("current_index", currentIndex),
zap.Int("new_index", newIndex)) zap.Int("new_index", newIndex))
return nil // No change needed return nil // No change needed
@@ -71,14 +72,14 @@ func (db *IndexableDB[T]) Reorder(ctx context.Context, objectRef bson.ObjectID,
if err != nil { if err != nil {
db.logger.Error("Failed to shift objects during reordering (moving down)", db.logger.Error("Failed to shift objects during reordering (moving down)",
zap.Error(err), zap.Error(err),
zap.String("object_ref", objectRef.Hex()), mzap.ObjRef("object_ref", objectRef),
zap.Int("current_index", currentIndex), zap.Int("current_index", currentIndex),
zap.Int("new_index", newIndex), zap.Int("new_index", newIndex),
zap.Int("updated_count", updatedCount)) zap.Int("updated_count", updatedCount))
return err return err
} }
db.logger.Debug("Successfully shifted objects (moving down)", db.logger.Debug("Successfully shifted objects (moving down)",
zap.String("object_ref", objectRef.Hex()), mzap.ObjRef("object_ref", objectRef),
zap.Int("updated_count", updatedCount)) zap.Int("updated_count", updatedCount))
} else { } else {
// Moving up: shift items between newIndex and currentIndex-1 down by +1 // Moving up: shift items between newIndex and currentIndex-1 down by +1
@@ -91,14 +92,14 @@ func (db *IndexableDB[T]) Reorder(ctx context.Context, objectRef bson.ObjectID,
if err != nil { if err != nil {
db.logger.Error("Failed to shift objects during reordering (moving up)", db.logger.Error("Failed to shift objects during reordering (moving up)",
zap.Error(err), zap.Error(err),
zap.String("object_ref", objectRef.Hex()), mzap.ObjRef("object_ref", objectRef),
zap.Int("current_index", currentIndex), zap.Int("current_index", currentIndex),
zap.Int("new_index", newIndex), zap.Int("new_index", newIndex),
zap.Int("updated_count", updatedCount)) zap.Int("updated_count", updatedCount))
return err return err
} }
db.logger.Debug("Successfully shifted objects (moving up)", db.logger.Debug("Successfully shifted objects (moving up)",
zap.String("object_ref", objectRef.Hex()), mzap.ObjRef("object_ref", objectRef),
zap.Int("updated_count", updatedCount)) zap.Int("updated_count", updatedCount))
} }
@@ -108,14 +109,14 @@ func (db *IndexableDB[T]) Reorder(ctx context.Context, objectRef bson.ObjectID,
if err != nil { if err != nil {
db.logger.Error("Failed to update target object index", db.logger.Error("Failed to update target object index",
zap.Error(err), zap.Error(err),
zap.String("object_ref", objectRef.Hex()), mzap.ObjRef("object_ref", objectRef),
zap.Int("current_index", currentIndex), zap.Int("current_index", currentIndex),
zap.Int("new_index", newIndex)) zap.Int("new_index", newIndex))
return err return err
} }
db.logger.Info("Successfully reordered object", db.logger.Info("Successfully reordered object",
zap.String("object_ref", objectRef.Hex()), mzap.ObjRef("object_ref", objectRef),
zap.Int("old_index", currentIndex), zap.Int("old_index", currentIndex),
zap.Int("new_index", newIndex)) zap.Int("new_index", newIndex))
return nil return nil

View File

@@ -5,20 +5,22 @@ import 'package:pshared/data/dto/payment/intent/payment.dart';
part 'initiate.g.dart'; part 'initiate.g.dart';
@JsonSerializable() @JsonSerializable()
class InitiatePaymentRequest extends PaymentBaseRequest { class InitiatePaymentRequest extends PaymentBaseRequest {
final PaymentIntentDTO? intent; final PaymentIntentDTO? intent;
final String? quoteRef; final String? quoteRef;
final String? clientPaymentRef;
const InitiatePaymentRequest({ const InitiatePaymentRequest({
required super.idempotencyKey, required super.idempotencyKey,
super.metadata, super.metadata,
this.intent, this.intent,
this.quoteRef, this.quoteRef,
this.clientPaymentRef,
}); });
factory InitiatePaymentRequest.fromJson(Map<String, dynamic> json) => _$InitiatePaymentRequestFromJson(json); factory InitiatePaymentRequest.fromJson(Map<String, dynamic> json) =>
_$InitiatePaymentRequestFromJson(json);
@override @override
Map<String, dynamic> toJson() => _$InitiatePaymentRequestToJson(this); Map<String, dynamic> toJson() => _$InitiatePaymentRequestToJson(this);
} }

View File

@@ -3,18 +3,20 @@ import 'package:pshared/api/requests/payment/base.dart';
part 'initiate_payments.g.dart'; part 'initiate_payments.g.dart';
@JsonSerializable() @JsonSerializable()
class InitiatePaymentsRequest extends PaymentBaseRequest { class InitiatePaymentsRequest extends PaymentBaseRequest {
final String quoteRef; final String quoteRef;
final String? clientPaymentRef;
const InitiatePaymentsRequest({ const InitiatePaymentsRequest({
required super.idempotencyKey, required super.idempotencyKey,
super.metadata, super.metadata,
required this.quoteRef, required this.quoteRef,
this.clientPaymentRef,
}); });
factory InitiatePaymentsRequest.fromJson(Map<String, dynamic> json) => _$InitiatePaymentsRequestFromJson(json); factory InitiatePaymentsRequest.fromJson(Map<String, dynamic> json) =>
_$InitiatePaymentsRequestFromJson(json);
@override @override
Map<String, dynamic> toJson() => _$InitiatePaymentsRequestToJson(this); Map<String, dynamic> toJson() => _$InitiatePaymentsRequestToJson(this);
} }

View File

@@ -7,7 +7,6 @@ import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/payment/multiple.dart'; import 'package:pshared/service/payment/multiple.dart';
import 'package:pshared/utils/exception.dart'; import 'package:pshared/utils/exception.dart';
class MultiPaymentProvider extends ChangeNotifier { class MultiPaymentProvider extends ChangeNotifier {
late OrganizationsProvider _organization; late OrganizationsProvider _organization;
late MultiQuotationProvider _quotation; late MultiQuotationProvider _quotation;
@@ -30,6 +29,7 @@ class MultiPaymentProvider extends ChangeNotifier {
Future<List<Payment>> pay({ Future<List<Payment>> pay({
String? idempotencyKey, String? idempotencyKey,
String? clientPaymentRef,
Map<String, String>? metadata, Map<String, String>? metadata,
}) async { }) async {
if (!_organization.isOrganizationSet) { if (!_organization.isOrganizationSet) {
@@ -52,6 +52,7 @@ class MultiPaymentProvider extends ChangeNotifier {
_organization.current.id, _organization.current.id,
quoteRef, quoteRef,
idempotencyKey: idempotencyKey, idempotencyKey: idempotencyKey,
clientPaymentRef: clientPaymentRef,
metadata: metadata, metadata: metadata,
); );

View File

@@ -7,12 +7,15 @@ import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/payment/service.dart'; import 'package:pshared/service/payment/service.dart';
import 'package:pshared/utils/exception.dart'; import 'package:pshared/utils/exception.dart';
class PaymentProvider extends ChangeNotifier { class PaymentProvider extends ChangeNotifier {
late OrganizationsProvider _organization; late OrganizationsProvider _organization;
late QuotationProvider _quotation; late QuotationProvider _quotation;
Resource<Payment> _payment = Resource(data: null, isLoading: false, error: null); Resource<Payment> _payment = Resource(
data: null,
isLoading: false,
error: null,
);
bool _isLoaded = false; bool _isLoaded = false;
void update(OrganizationsProvider organization, QuotationProvider quotation) { void update(OrganizationsProvider organization, QuotationProvider quotation) {
@@ -23,15 +26,21 @@ class PaymentProvider extends ChangeNotifier {
Payment? get payment => _payment.data; Payment? get payment => _payment.data;
bool get isLoading => _payment.isLoading; bool get isLoading => _payment.isLoading;
Exception? get error => _payment.error; Exception? get error => _payment.error;
bool get isReady => _isLoaded && !_payment.isLoading && _payment.error == null; bool get isReady =>
_isLoaded && !_payment.isLoading && _payment.error == null;
void _setResource(Resource<Payment> payment) { void _setResource(Resource<Payment> payment) {
_payment = payment; _payment = payment;
notifyListeners(); notifyListeners();
} }
Future<Payment?> pay({String? idempotencyKey, Map<String, String>? metadata}) async { Future<Payment?> pay({
if (!_organization.isOrganizationSet) throw StateError('Organization is not set'); String? idempotencyKey,
String? clientPaymentRef,
Map<String, String>? metadata,
}) async {
if (!_organization.isOrganizationSet)
throw StateError('Organization is not set');
final quoteRef = _quotation.quotation?.quoteRef; final quoteRef = _quotation.quotation?.quoteRef;
if (quoteRef == null || quoteRef.isEmpty) { if (quoteRef == null || quoteRef.isEmpty) {
throw StateError('Quotation reference is not set'); throw StateError('Quotation reference is not set');
@@ -49,12 +58,17 @@ class PaymentProvider extends ChangeNotifier {
_organization.current.id, _organization.current.id,
quoteRef, quoteRef,
idempotencyKey: resolvedIdempotencyKey, idempotencyKey: resolvedIdempotencyKey,
clientPaymentRef: clientPaymentRef,
metadata: metadata, metadata: metadata,
); );
_isLoaded = true; _isLoaded = true;
_setResource(_payment.copyWith(data: response, isLoading: false, error: null)); _setResource(
_payment.copyWith(data: response, isLoading: false, error: null),
);
} catch (e) { } catch (e) {
_setResource(_payment.copyWith(data: null, error: toException(e), isLoading: false)); _setResource(
_payment.copyWith(data: null, error: toException(e), isLoading: false),
);
} }
return _payment.data; return _payment.data;
} }

View File

@@ -13,7 +13,6 @@ import 'package:pshared/models/payment/quote/quotes.dart';
import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart'; import 'package:pshared/service/services.dart';
class MultiplePaymentsService { class MultiplePaymentsService {
static final _logger = Logger('service.payment.multiple'); static final _logger = Logger('service.payment.multiple');
static const String _objectType = Services.payments; static const String _objectType = Services.payments;
@@ -37,6 +36,7 @@ class MultiplePaymentsService {
String organizationRef, String organizationRef,
String quoteRef, { String quoteRef, {
String? idempotencyKey, String? idempotencyKey,
String? clientPaymentRef,
Map<String, String>? metadata, Map<String, String>? metadata,
}) async { }) async {
_logger.fine( _logger.fine(
@@ -45,6 +45,7 @@ class MultiplePaymentsService {
final request = InitiatePaymentsRequest( final request = InitiatePaymentsRequest(
idempotencyKey: idempotencyKey ?? const Uuid().v4(), idempotencyKey: idempotencyKey ?? const Uuid().v4(),
quoteRef: quoteRef, quoteRef: quoteRef,
clientPaymentRef: clientPaymentRef,
metadata: metadata, metadata: metadata,
); );

View File

@@ -82,6 +82,7 @@ class PaymentService {
String organizationRef, String organizationRef,
String quotationRef, { String quotationRef, {
String? idempotencyKey, String? idempotencyKey,
String? clientPaymentRef,
Map<String, String>? metadata, Map<String, String>? metadata,
}) async { }) async {
_logger.fine( _logger.fine(
@@ -90,6 +91,7 @@ class PaymentService {
final request = InitiatePaymentRequest( final request = InitiatePaymentRequest(
idempotencyKey: idempotencyKey ?? Uuid().v4(), idempotencyKey: idempotencyKey ?? Uuid().v4(),
quoteRef: quotationRef, quoteRef: quotationRef,
clientPaymentRef: clientPaymentRef,
metadata: metadata, metadata: metadata,
); );
final response = await AuthorizationService.getPOSTResponse( final response = await AuthorizationService.getPOSTResponse(

View File

@@ -158,16 +158,13 @@ void main() {
final request = InitiatePaymentRequest( final request = InitiatePaymentRequest(
idempotencyKey: 'idem-2', idempotencyKey: 'idem-2',
quoteRef: 'q-1', quoteRef: 'q-1',
metadata: const {'client_payment_ref': 'cp-1'}, clientPaymentRef: 'cp-1',
); );
final json = request.toJson(); final json = request.toJson();
expect(json['idempotencyKey'], equals('idem-2')); expect(json['idempotencyKey'], equals('idem-2'));
expect(json['quoteRef'], equals('q-1')); expect(json['quoteRef'], equals('q-1'));
expect( expect(json['clientPaymentRef'], equals('cp-1'));
(json['metadata'] as Map<String, dynamic>)['client_payment_ref'],
equals('cp-1'),
);
expect(json.containsKey('intent'), isTrue); expect(json.containsKey('intent'), isTrue);
expect(json['intent'], isNull); expect(json['intent'], isNull);
}); });
@@ -176,16 +173,13 @@ void main() {
final request = InitiatePaymentsRequest( final request = InitiatePaymentsRequest(
idempotencyKey: 'idem-3', idempotencyKey: 'idem-3',
quoteRef: 'q-2', quoteRef: 'q-2',
metadata: const {'client_payment_ref': 'cp-1'}, clientPaymentRef: 'cp-1',
); );
final json = request.toJson(); final json = request.toJson();
expect(json['idempotencyKey'], equals('idem-3')); expect(json['idempotencyKey'], equals('idem-3'));
expect(json['quoteRef'], equals('q-2')); expect(json['quoteRef'], equals('q-2'));
expect( expect(json['clientPaymentRef'], equals('cp-1'));
(json['metadata'] as Map<String, dynamic>)['client_payment_ref'],
equals('cp-1'),
);
expect(json.containsKey('intentRef'), isFalse); expect(json.containsKey('intentRef'), isFalse);
expect(json.containsKey('intentRefs'), isFalse); expect(json.containsKey('intentRefs'), isFalse);
}); });

View File

@@ -107,6 +107,9 @@ components:
quoteRef: quoteRef:
description: Reference to a previously generated quote to execute. description: Reference to a previously generated quote to execute.
type: string type: string
clientPaymentRef:
description: Optional caller-side payment correlation reference.
type: string
InitiatePaymentsRequest: InitiatePaymentsRequest:
description: Request payload to initiate multiple payments from a multi-quote reference. description: Request payload to initiate multiple payments from a multi-quote reference.
@@ -120,6 +123,9 @@ components:
quoteRef: quoteRef:
description: Reference to a previously generated multi-quote. description: Reference to a previously generated multi-quote.
type: string type: string
clientPaymentRef:
description: Optional caller-side payment correlation reference.
type: string
InitiatePaymentByQuoteRequest: InitiatePaymentByQuoteRequest:
description: Request payload to initiate one payment from an existing quote reference. description: Request payload to initiate one payment from an existing quote reference.
@@ -133,3 +139,6 @@ components:
quoteRef: quoteRef:
description: Reference to a previously generated quote to execute. description: Reference to a previously generated quote to execute.
type: string type: string
clientPaymentRef:
description: Optional caller-side payment correlation reference.
type: string