From 0f95f898a844d32b90caf0c676d8d81fb842d892 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 26 Feb 2026 16:59:09 +0100 Subject: [PATCH] gRPC error translation: invalid argument support --- .../internal/service/orchestrator/service.go | 2 +- .../service/orchestrator/service_v2.go | 32 ++++++-- .../service/orchestrator/service_v2_test.go | 78 ++++++------------- .../server/paymentapiimp/grpc_error.go | 41 ++++++++++ .../internal/server/paymentapiimp/list.go | 2 +- .../internal/server/paymentapiimp/pay.go | 4 +- .../internal/server/paymentapiimp/paybatch.go | 2 +- .../internal/server/paymentapiimp/quote.go | 4 +- 8 files changed, 99 insertions(+), 66 deletions(-) create mode 100644 api/server/internal/server/paymentapiimp/grpc_error.go diff --git a/api/payments/orchestrator/internal/service/orchestrator/service.go b/api/payments/orchestrator/internal/service/orchestrator/service.go index 0d4d1b1b..7237537e 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service.go @@ -72,7 +72,7 @@ func (s *Service) Register(router routers.GRPC) error { return nil } return router.Register(func(reg grpc.ServiceRegistrar) { - orchestrationv2.RegisterPaymentOrchestratorServiceServer(reg, newV2GRPCServer(s.v2)) + orchestrationv2.RegisterPaymentOrchestratorServiceServer(reg, newV2GRPCServer(s.v2, s.logger)) }) } diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_v2.go b/api/payments/orchestrator/internal/service/orchestrator/service_v2.go index 639b7c9f..53c1df88 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_v2.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_v2.go @@ -11,6 +11,7 @@ import ( "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec" "github.com/tech/sendico/payments/storage" + "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" @@ -145,21 +146,40 @@ func buildPaymentRepositoryV2(repo storage.Repository, logger mlogger.Logger) (p type v2GRPCServer struct { orchestrationv2.UnimplementedPaymentOrchestratorServiceServer - svc psvc.Service + svc psvc.Service + logger mlogger.Logger } -func newV2GRPCServer(svc psvc.Service) *v2GRPCServer { - return &v2GRPCServer{svc: svc} +func newV2GRPCServer(svc psvc.Service, logger mlogger.Logger) *v2GRPCServer { + if logger == nil { + logger = zap.NewNop() + } + return &v2GRPCServer{ + svc: svc, + logger: logger.Named("grpc"), + } } func (s *v2GRPCServer) ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error) { - return s.svc.ExecutePayment(ctx, req) + resp, err := s.svc.ExecutePayment(ctx, req) + if err != nil { + return gsresponse.Execute(ctx, gsresponse.Auto[orchestrationv2.ExecutePaymentResponse](s.logger, mservice.PaymentOrchestrator, err)) + } + return resp, nil } func (s *v2GRPCServer) GetPayment(ctx context.Context, req *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error) { - return s.svc.GetPayment(ctx, req) + resp, err := s.svc.GetPayment(ctx, req) + if err != nil { + return gsresponse.Execute(ctx, gsresponse.Auto[orchestrationv2.GetPaymentResponse](s.logger, mservice.PaymentOrchestrator, err)) + } + return resp, nil } func (s *v2GRPCServer) ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error) { - return s.svc.ListPayments(ctx, req) + resp, err := s.svc.ListPayments(ctx, req) + if err != nil { + return gsresponse.Execute(ctx, gsresponse.Auto[orchestrationv2.ListPaymentsResponse](s.logger, mservice.PaymentOrchestrator, err)) + } + return resp, nil } diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_v2_test.go b/api/payments/orchestrator/internal/service/orchestrator/service_v2_test.go index 9eed5915..cac2220d 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_v2_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_v2_test.go @@ -5,75 +5,47 @@ import ( "strings" "testing" - ledgerclient "github.com/tech/sendico/ledger/client" - "github.com/tech/sendico/payments/storage" - quotestorage "github.com/tech/sendico/payments/storage/quote" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc" + "github.com/tech/sendico/pkg/merrors" + orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) -func TestNewOrchestrationV2Service_FailsWhenLedgerClientMissing(t *testing.T) { - svc, repo, err := newOrchestrationV2Service(zap.NewNop(), fakeStorageRepo{}, v2RuntimeDeps{}) +func TestV2GRPCServerExecutePayment_MapsInvalidArgument(t *testing.T) { + srv := newV2GRPCServer(fakeV2Service{ + executeErr: merrors.InvalidArgument("intent_ref is required for batch quotation"), + }, zap.NewNop()) + + _, err := srv.ExecutePayment(context.Background(), &orchestrationv2.ExecutePaymentRequest{}) if err == nil { t.Fatal("expected error") } - if !strings.Contains(err.Error(), "ledger client is required") { - t.Fatalf("unexpected error: %v", err) + if got, want := status.Code(err), codes.InvalidArgument; got != want { + t.Fatalf("unexpected grpc status code: got=%s want=%s", got, want) } - if svc != nil { - t.Fatal("expected nil service") - } - if repo != nil { - t.Fatal("expected nil payment repo") + if got := status.Convert(err).Message(); !strings.Contains(got, "intent_ref is required for batch quotation") { + t.Fatalf("unexpected grpc status message: %q", got) } } -func TestNewOrchestrationV2Service_FailsWhenLedgerClientUnavailable(t *testing.T) { - ledger := unavailableLedgerClient{Fake: &ledgerclient.Fake{}} - svc, repo, err := newOrchestrationV2Service(zap.NewNop(), fakeStorageRepo{}, v2RuntimeDeps{ - LedgerClient: ledger, - }) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), "ledger client is unavailable") { - t.Fatalf("unexpected error: %v", err) - } - if svc != nil { - t.Fatal("expected nil service") - } - if repo != nil { - t.Fatal("expected nil payment repo") - } +type fakeV2Service struct { + executeErr error } -type unavailableLedgerClient struct { - *ledgerclient.Fake +func (f fakeV2Service) ExecutePayment(context.Context, *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error) { + return nil, f.executeErr } -func (u unavailableLedgerClient) Available() bool { - return false +func (fakeV2Service) GetPayment(context.Context, *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error) { + return &orchestrationv2.GetPaymentResponse{}, nil } -type fakeStorageRepo struct{} - -func (fakeStorageRepo) Ping(context.Context) error { - return nil +func (fakeV2Service) ListPayments(context.Context, *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error) { + return &orchestrationv2.ListPaymentsResponse{}, nil } -func (fakeStorageRepo) Payments() storage.PaymentsStore { - return nil +func (fakeV2Service) ReconcileExternal(context.Context, psvc.ReconcileExternalInput) (*psvc.ReconcileExternalOutput, error) { + return &psvc.ReconcileExternalOutput{}, nil } - -func (fakeStorageRepo) PaymentMethods() storage.PaymentMethodsStore { - return nil -} - -func (fakeStorageRepo) Quotes() quotestorage.QuotesStore { - return nil -} - -func (fakeStorageRepo) Routes() storage.RoutesStore { - return nil -} - -var _ storage.Repository = fakeStorageRepo{} diff --git a/api/server/internal/server/paymentapiimp/grpc_error.go b/api/server/internal/server/paymentapiimp/grpc_error.go new file mode 100644 index 00000000..7d058ece --- /dev/null +++ b/api/server/internal/server/paymentapiimp/grpc_error.go @@ -0,0 +1,41 @@ +package paymentapiimp + +import ( + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func grpcErrorResponse(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc { + statusErr, ok := status.FromError(err) + if !ok { + return response.Internal(logger, source, err) + } + + switch statusErr.Code() { + case codes.InvalidArgument: + return response.BadRequest(logger, source, "invalid_argument", statusErr.Message()) + case codes.NotFound: + return response.NotFound(logger, source, statusErr.Message()) + case codes.PermissionDenied: + return response.AccessDenied(logger, source, statusErr.Message()) + case codes.Unauthenticated: + return response.Unauthorized(logger, source, statusErr.Message()) + case codes.AlreadyExists, codes.Aborted: + return response.DataConflict(logger, source, statusErr.Message()) + case codes.Unimplemented: + return response.NotImplemented(logger, source, statusErr.Message()) + case codes.FailedPrecondition: + return response.Error(logger, source, http.StatusPreconditionFailed, "failed_precondition", statusErr.Message()) + case codes.DeadlineExceeded: + return response.Error(logger, source, http.StatusGatewayTimeout, "deadline_exceeded", statusErr.Message()) + case codes.Unavailable: + return response.Error(logger, source, http.StatusServiceUnavailable, "service_unavailable", statusErr.Message()) + default: + return response.Internal(logger, source, err) + } +} diff --git a/api/server/internal/server/paymentapiimp/list.go b/api/server/internal/server/paymentapiimp/list.go index b1362823..158b3965 100644 --- a/api/server/internal/server/paymentapiimp/list.go +++ b/api/server/internal/server/paymentapiimp/list.go @@ -80,7 +80,7 @@ func (a *PaymentAPI) listPayments(r *http.Request, account *model.Account, token resp, err := a.execution.ListPayments(ctx, req) if err != nil { a.logger.Warn("Failed to list payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef)) - return response.Auto(a.logger, a.Name(), err) + return grpcErrorResponse(a.logger, a.Name(), err) } return sresponse.PaymentsListResponse(a.logger, resp, token) diff --git a/api/server/internal/server/paymentapiimp/pay.go b/api/server/internal/server/paymentapiimp/pay.go index f5e680c5..42e98468 100644 --- a/api/server/internal/server/paymentapiimp/pay.go +++ b/api/server/internal/server/paymentapiimp/pay.go @@ -76,7 +76,7 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to }) if qErr != nil { a.logger.Warn("Failed to quote payment before execution", zap.Error(qErr), mzap.ObjRef("organization_ref", orgRef)) - return response.Auto(a.logger, a.Name(), qErr) + return grpcErrorResponse(a.logger, a.Name(), qErr) } quotationRef = strings.TrimSpace(quoteResp.GetQuote().GetQuoteRef()) if quotationRef == "" { @@ -97,7 +97,7 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to resp, err := a.execution.ExecutePayment(ctx, req) if err != nil { a.logger.Warn("Failed to initiate payment", zap.Error(err), mzap.ObjRef("organization_ref", orgRef)) - return response.Auto(a.logger, a.Name(), err) + return grpcErrorResponse(a.logger, a.Name(), err) } return sresponse.PaymentResponse(a.logger, resp.GetPayment(), token) diff --git a/api/server/internal/server/paymentapiimp/paybatch.go b/api/server/internal/server/paymentapiimp/paybatch.go index 7b1fe533..27db08e1 100644 --- a/api/server/internal/server/paymentapiimp/paybatch.go +++ b/api/server/internal/server/paymentapiimp/paybatch.go @@ -49,7 +49,7 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc resp, err := a.execution.ExecutePayment(ctx, req) if err != nil { a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) - return response.Auto(a.logger, a.Name(), err) + return grpcErrorResponse(a.logger, a.Name(), err) } payments := make([]*orchestrationv2.Payment, 0, 1) diff --git a/api/server/internal/server/paymentapiimp/quote.go b/api/server/internal/server/paymentapiimp/quote.go index 56cf1119..f9b91bcd 100644 --- a/api/server/internal/server/paymentapiimp/quote.go +++ b/api/server/internal/server/paymentapiimp/quote.go @@ -62,7 +62,7 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token resp, err := a.quotation.QuotePayment(ctx, req) if err != nil { a.logger.Warn("Failed to quote payment", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) - return response.Auto(a.logger, a.Name(), err) + return grpcErrorResponse(a.logger, a.Name(), err) } return sresponse.PaymentQuoteResponse(a.logger, resp.GetIdempotencyKey(), resp.GetQuote(), token) @@ -118,7 +118,7 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke resp, err := a.quotation.QuotePayments(ctx, req) if err != nil { a.logger.Warn("Failed to quote payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) - return response.Auto(a.logger, a.Name(), err) + return grpcErrorResponse(a.logger, a.Name(), err) } return sresponse.PaymentQuotesResponse(a.logger, resp, token)