package client import ( "context" "crypto/tls" "errors" "fmt" "strings" "time" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" ) // Client exposes typed helpers around the ledger gRPC API. type Client interface { PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) Close() error } type grpcLedgerClient interface { PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) ApplyFXWithCharges(ctx context.Context, in *ledgerv1.FXRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) GetBalance(ctx context.Context, in *ledgerv1.GetBalanceRequest, opts ...grpc.CallOption) (*ledgerv1.BalanceResponse, error) GetJournalEntry(ctx context.Context, in *ledgerv1.GetEntryRequest, opts ...grpc.CallOption) (*ledgerv1.JournalEntryResponse, error) GetStatement(ctx context.Context, in *ledgerv1.GetStatementRequest, opts ...grpc.CallOption) (*ledgerv1.StatementResponse, error) } type ledgerClient struct { cfg Config conn *grpc.ClientConn client grpcLedgerClient } // New dials the ledger 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, errors.New("ledger: address is required") } 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, cfg.Address, dialOpts...) if err != nil { return nil, fmt.Errorf("ledger: dial %s: %w", cfg.Address, err) } return &ledgerClient{ cfg: cfg, conn: conn, client: ledgerv1.NewLedgerServiceClient(conn), }, nil } // NewWithClient injects a pre-built ledger client (useful for tests). func NewWithClient(cfg Config, lc grpcLedgerClient) Client { cfg.setDefaults() return &ledgerClient{ cfg: cfg, client: lc, } } func (c *ledgerClient) Close() error { if c.conn != nil { return c.conn.Close() } return nil } func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.PostCreditWithCharges(ctx, req) } func (c *ledgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.PostDebitWithCharges(ctx, req) } func (c *ledgerClient) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.TransferInternal(ctx, req) } func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.ApplyFXWithCharges(ctx, req) } func (c *ledgerClient) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.GetBalance(ctx, req) } func (c *ledgerClient) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.GetJournalEntry(ctx, req) } func (c *ledgerClient) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) { ctx, cancel := c.callContext(ctx) defer cancel() return c.client.GetStatement(ctx, req) } func (c *ledgerClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { timeout := c.cfg.CallTimeout if timeout <= 0 { timeout = 3 * time.Second } return context.WithTimeout(ctx, timeout) }