package client import ( "context" "crypto/tls" "fmt" "strings" "time" "github.com/tech/sendico/pkg/merrors" orchestrationv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" ) // Client exposes typed helpers around the payment orchestration and quotation gRPC APIs. type Client interface { QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error) QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) Close() error } type grpcOrchestratorClient interface { InitiatePayments(ctx context.Context, in *orchestratorv1.InitiatePaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentsResponse, error) InitiatePayment(ctx context.Context, in *orchestratorv1.InitiatePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentResponse, error) CancelPayment(ctx context.Context, in *orchestratorv1.CancelPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.CancelPaymentResponse, error) GetPayment(ctx context.Context, in *orchestratorv1.GetPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.GetPaymentResponse, error) ListPayments(ctx context.Context, in *orchestratorv1.ListPaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.ListPaymentsResponse, error) InitiateConversion(ctx context.Context, in *orchestratorv1.InitiateConversionRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiateConversionResponse, error) ProcessTransferUpdate(ctx context.Context, in *orchestratorv1.ProcessTransferUpdateRequest, opts ...grpc.CallOption) (*orchestratorv1.ProcessTransferUpdateResponse, error) ProcessDepositObserved(ctx context.Context, in *orchestratorv1.ProcessDepositObservedRequest, opts ...grpc.CallOption) (*orchestratorv1.ProcessDepositObservedResponse, error) } type grpcQuotationClient interface { QuotePayment(ctx context.Context, in *orchestratorv1.QuotePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.QuotePaymentResponse, error) QuotePayments(ctx context.Context, in *orchestratorv1.QuotePaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.QuotePaymentsResponse, error) } type orchestratorClient struct { cfg Config conn *grpc.ClientConn quoteConn *grpc.ClientConn client grpcOrchestratorClient quoteClient grpcQuotationClient } // New dials the payment orchestrator endpoint and returns a ready client. func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { cfg.setDefaults() if strings.TrimSpace(cfg.Address) == "" { return nil, merrors.InvalidArgument("payment-orchestrator: address is required") } if strings.TrimSpace(cfg.QuoteAddress) == "" { cfg.QuoteAddress = cfg.Address } conn, err := dial(ctx, cfg, cfg.Address, opts...) if err != nil { return nil, err } quoteConn := conn if cfg.QuoteAddress != cfg.Address { quoteConn, err = dial(ctx, cfg, cfg.QuoteAddress, opts...) if err != nil { _ = conn.Close() return nil, err } } return &orchestratorClient{ cfg: cfg, conn: conn, quoteConn: quoteConn, client: orchestrationv1.NewPaymentExecutionServiceClient(conn), quoteClient: quotationv1.NewQuotationServiceClient(quoteConn), }, nil } func dial(ctx context.Context, cfg Config, address string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) defer cancel() dialOpts := make([]grpc.DialOption, 0, len(opts)+1) dialOpts = append(dialOpts, opts...) if cfg.Insecure { dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) } else { dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) } conn, err := grpc.DialContext(dialCtx, address, dialOpts...) if err != nil { return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-orchestrator: dial %s", address)) } return conn, nil } // NewWithClient injects a pre-built orchestrator client (useful for tests). func NewWithClient(cfg Config, oc grpcOrchestratorClient) Client { return NewWithClients(cfg, oc, nil) } // NewWithClients injects pre-built orchestrator and quotation clients (useful for tests). func NewWithClients(cfg Config, oc grpcOrchestratorClient, qc grpcQuotationClient) Client { cfg.setDefaults() if qc == nil { if q, ok := any(oc).(grpcQuotationClient); ok { qc = q } } return &orchestratorClient{ cfg: cfg, client: oc, quoteClient: qc, } } func (c *orchestratorClient) Close() error { var firstErr error if c.quoteConn != nil && c.quoteConn != c.conn { if err := c.quoteConn.Close(); err != nil && firstErr == nil { firstErr = err } } if c.conn != nil { if err := c.conn.Close(); err != nil && firstErr == nil { firstErr = err } } return firstErr } func (c *orchestratorClient) QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error) { if c.quoteClient == nil { return nil, merrors.InvalidArgument("payment-orchestrator: quotation client is not configured") } ctx, cancel := c.callContext(ctx) defer cancel() return c.quoteClient.QuotePayment(ctx, req) } func (c *orchestratorClient) QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error) { if c.quoteClient == nil { return nil, merrors.InvalidArgument("payment-orchestrator: quotation client is not configured") } ctx, cancel := c.callContext(ctx) defer cancel() return c.quoteClient.QuotePayments(ctx, req) } func (c *orchestratorClient) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.InitiatePayments(ctx, req) } func (c *orchestratorClient) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.InitiatePayment(ctx, req) } func (c *orchestratorClient) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.CancelPayment(ctx, req) } func (c *orchestratorClient) GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.GetPayment(ctx, req) } func (c *orchestratorClient) ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.ListPayments(ctx, req) } func (c *orchestratorClient) InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.InitiateConversion(ctx, req) } func (c *orchestratorClient) ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.ProcessTransferUpdate(ctx, req) } func (c *orchestratorClient) ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.ProcessDepositObserved(ctx, req) } func (c *orchestratorClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { timeout := c.cfg.CallTimeout if timeout <= 0 { timeout = 3 * time.Second } return context.WithTimeout(ctx, timeout) }