ledger account describibale support

This commit is contained in:
Stephan D
2026-01-06 17:51:35 +01:00
parent 12700c5595
commit 43edbc109d
34 changed files with 326 additions and 91 deletions

View File

@@ -373,6 +373,20 @@ func managedWalletFromAccount(account *connectorv1.Account) *chainv1.ManagedWall
if asset.GetTokenSymbol() == "" {
asset.TokenSymbol = strings.TrimSpace(tokenFromAssetString(account.GetAsset()))
}
describable := account.GetDescribable()
label := strings.TrimSpace(account.GetLabel())
if describable == nil {
if label != "" {
describable = &describablev1.Describable{Name: label}
}
} else if strings.TrimSpace(describable.GetName()) == "" && label != "" {
desc := strings.TrimSpace(describable.GetDescription())
if desc == "" {
describable = &describablev1.Describable{Name: label}
} else {
describable = &describablev1.Describable{Name: label, Description: &desc}
}
}
return &chainv1.ManagedWallet{
WalletRef: walletRef,
OrganizationRef: organizationRef,
@@ -382,9 +396,7 @@ func managedWalletFromAccount(account *connectorv1.Account) *chainv1.ManagedWall
Status: managedWalletStatusFromAccount(account.GetState()),
CreatedAt: account.GetCreatedAt(),
UpdatedAt: account.GetUpdatedAt(),
Describable: &describablev1.Describable{
Name: strings.TrimSpace(account.GetLabel()),
},
Describable: describable,
}
}

View File

@@ -376,6 +376,7 @@ func chainWalletToAccount(wallet *chainv1.ManagedWallet) *connectorv1.Account {
ProviderDetails: details,
CreatedAt: wallet.GetCreatedAt(),
UpdatedAt: wallet.GetUpdatedAt(),
Describable: wallet.GetDescribable(),
}
}
@@ -394,7 +395,7 @@ func chainWalletState(status chainv1.ManagedWalletStatus) connectorv1.AccountSta
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
if op == nil {
return nil, fmt.Errorf("transfer: operation is required")
return nil, merrors.InvalidArgument("transfer: operation is required")
}
if to := op.GetTo(); to != nil {
if account := to.GetAccount(); account != nil {
@@ -404,7 +405,7 @@ func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.Trans
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
}
}
return nil, fmt.Errorf("transfer: to.account or to.external is required")
return nil, merrors.InvalidArgument("transfer: to.account or to.external is required")
}
func normalizeMoneyForChain(m *moneyv1.Money) *moneyv1.Money {
@@ -451,12 +452,12 @@ func parseChainFees(reader params.Reader) []*chainv1.ServiceFeeBreakdown {
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
if raw == nil {
return nil, fmt.Errorf("money is required")
return nil, merrors.InvalidArgument("money is required")
}
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
if amount == "" || currency == "" {
return nil, fmt.Errorf("money is required")
return nil, merrors.InvalidArgument("money is required")
}
return &moneyv1.Money{
Amount: amount,
@@ -575,11 +576,11 @@ func parseChainAsset(assetString string, reader params.Reader) (*chainv1.Asset,
network = networkFromAssetString(assetString)
}
if token == "" {
return nil, fmt.Errorf("asset: token_symbol is required")
return nil, merrors.InvalidArgument("asset: token_symbol is required")
}
chain := shared.ChainEnumFromName(network)
if chain == chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED {
return nil, fmt.Errorf("asset: network is required")
return nil, merrors.InvalidArgument("asset: network is required")
}
return &chainv1.Asset{
Chain: chain,

View File

@@ -1,15 +1,16 @@
package shared
import (
"errors"
"math/big"
"strings"
"github.com/tech/sendico/pkg/merrors"
)
var (
errHexEmpty = errors.New("hex value is empty")
errHexInvalid = errors.New("invalid hex number")
errHexOutOfRange = errors.New("hex number out of range")
errHexEmpty = merrors.InvalidArgument("hex value is empty")
errHexInvalid = merrors.InvalidArgument("invalid hex number")
errHexOutOfRange = merrors.InvalidArgument("hex number out of range")
)
// DecodeHexBig parses a hex string that may include leading zero digits.

View File

@@ -458,7 +458,7 @@ func (i *Imp) resolveCallbackConfig(cfg callbackConfig) (callbackRuntimeConfig,
func (i *Imp) startHTTPCallbackServer(svc *mntxservice.Service, cfg callbackRuntimeConfig) error {
if svc == nil {
return errors.New("nil service provided for callback server")
return merrors.InvalidArgument("nil service provided for callback server")
}
if strings.TrimSpace(cfg.Address) == "" {
i.logger.Info("Monetix callback server disabled: address is empty")

View File

@@ -3,7 +3,6 @@ package gateway
import (
"context"
"errors"
"fmt"
"strings"
"github.com/shopspring/decimal"
@@ -124,26 +123,26 @@ func mntxOperationParams() []*connectorv1.OperationParamSpec {
func payoutAmount(op *connectorv1.Operation, reader params.Reader) (int64, string, error) {
if op == nil {
return 0, "", fmt.Errorf("payout: operation is required")
return 0, "", merrors.InvalidArgument("payout: operation is required")
}
currency := currencyFromOperation(op)
if currency == "" {
return 0, "", fmt.Errorf("payout: currency is required")
return 0, "", merrors.InvalidArgument("payout: currency is required")
}
if minor, ok := reader.Int64("amount_minor"); ok && minor > 0 {
return minor, currency, nil
}
money := op.GetMoney()
if money == nil {
return 0, "", fmt.Errorf("payout: money is required")
return 0, "", merrors.InvalidArgument("payout: money is required")
}
amount := strings.TrimSpace(money.GetAmount())
if amount == "" {
return 0, "", fmt.Errorf("payout: amount is required")
return 0, "", merrors.InvalidArgument("payout: amount is required")
}
dec, err := decimal.NewFromString(amount)
if err != nil {
return 0, "", fmt.Errorf("payout: invalid amount")
return 0, "", merrors.InvalidArgument("payout: invalid amount")
}
minor := dec.Mul(decimal.NewFromInt(100)).IntPart()
return minor, currency, nil

View File

@@ -3,7 +3,6 @@ package gateway
import (
"context"
"errors"
"fmt"
"strings"
"github.com/tech/sendico/pkg/connector/params"
@@ -139,7 +138,7 @@ func tgsettleOperationParams() []*connectorv1.OperationParamSpec {
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
if op == nil {
return nil, fmt.Errorf("transfer: operation is required")
return nil, merrors.InvalidArgument("transfer: operation is required")
}
if to := op.GetTo(); to != nil {
if account := to.GetAccount(); account != nil {
@@ -149,7 +148,7 @@ func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.Trans
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
}
}
return nil, fmt.Errorf("transfer: to.account or to.external is required")
return nil, merrors.InvalidArgument("transfer: to.account or to.external is required")
}
func normalizeMoneyForTransfer(m *moneyv1.Money) *moneyv1.Money {

View File

@@ -2,12 +2,12 @@ package storage
import (
"context"
"errors"
"github.com/tech/sendico/gateway/tgsettle/storage/model"
"github.com/tech/sendico/pkg/merrors"
)
var ErrDuplicate = errors.New("payment gateway storage: duplicate record")
var ErrDuplicate = merrors.DataConflict("payment gateway storage: duplicate record")
type Repository interface {
Payments() PaymentsStore

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/payments/rail"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
@@ -196,12 +197,23 @@ func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAc
"allow_negative": req.GetAllowNegative(),
"is_settlement": req.GetIsSettlement(),
}
label := ""
if desc := req.GetDescribable(); desc != nil {
label = strings.TrimSpace(desc.GetName())
if desc.Description != nil {
trimmed := strings.TrimSpace(desc.GetDescription())
if trimmed != "" {
params["description"] = trimmed
}
}
}
if len(req.GetMetadata()) > 0 {
params["metadata"] = mapStringToInterface(req.GetMetadata())
}
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
Asset: strings.TrimSpace(req.GetCurrency()),
Label: label,
Params: structFromMap(params),
})
if err != nil {
@@ -469,6 +481,18 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
if ref := account.GetRef(); ref != nil {
accountID = strings.TrimSpace(ref.GetAccountId())
}
describable := account.GetDescribable()
label := strings.TrimSpace(account.GetLabel())
if describable == nil && label != "" {
describable = &describablev1.Describable{Name: label}
} else if describable != nil && strings.TrimSpace(describable.GetName()) == "" && label != "" {
desc := strings.TrimSpace(describable.GetDescription())
if desc == "" {
describable = &describablev1.Describable{Name: label}
} else {
describable = &describablev1.Describable{Name: label, Description: &desc}
}
}
return &ledgerv1.LedgerAccount{
LedgerAccountRef: accountID,
OrganizationRef: strings.TrimSpace(account.GetOwnerRef()),
@@ -480,6 +504,7 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
IsSettlement: isSettlement,
CreatedAt: account.GetCreatedAt(),
UpdatedAt: account.GetUpdatedAt(),
Describable: describable,
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/ledger/storage/model"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -62,6 +63,8 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
metadata = nil
}
describable := describableFromProto(req.GetDescribable())
account := &model.Account{
AccountCode: accountCode,
Currency: currency,
@@ -71,6 +74,9 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
IsSettlement: req.GetIsSettlement(),
Metadata: metadata,
}
if describable != nil {
account.Describable = *describable
}
account.OrganizationRef = orgRef
err = s.storage.Accounts().Create(ctx, account)
@@ -204,5 +210,45 @@ func toProtoAccount(account *model.Account) *ledgerv1.LedgerAccount {
Metadata: metadata,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
Describable: describableToProto(account.Describable),
}
}
func describableFromProto(desc *describablev1.Describable) *model.Describable {
if desc == nil {
return nil
}
name := strings.TrimSpace(desc.GetName())
var description *string
if desc.Description != nil {
trimmed := strings.TrimSpace(desc.GetDescription())
if trimmed != "" {
description = &trimmed
}
}
if name == "" && description == nil {
return nil
}
return &model.Describable{
Name: name,
Description: description,
}
}
func describableToProto(desc model.Describable) *describablev1.Describable {
name := strings.TrimSpace(desc.Name)
var description *string
if desc.Description != nil {
trimmed := strings.TrimSpace(*desc.Description)
if trimmed != "" {
description = &trimmed
}
}
if name == "" && description == nil {
return nil
}
return &describablev1.Describable{
Name: name,
Description: description,
}
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/tech/sendico/ledger/internal/appversion"
"github.com/tech/sendico/pkg/connector/params"
"github.com/tech/sendico/pkg/merrors"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
@@ -76,6 +77,7 @@ func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.Ope
status := parseLedgerAccountStatus(reader, "status")
metadata := mergeMetadata(reader.StringMap("metadata"), req.GetLabel(), req.GetOwnerRef(), req.GetCorrelationId(), req.GetParentIntentId())
describable := describableFromLabel(req.GetLabel(), reader.String("description"))
resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
OrganizationRef: orgRef,
@@ -86,6 +88,7 @@ func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.Ope
AllowNegative: reader.Bool("allow_negative"),
IsSettlement: reader.Bool("is_settlement"),
Metadata: metadata,
Describable: describable,
})
if err != nil {
return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil
@@ -340,6 +343,7 @@ func ledgerAccountToConnector(account *ledgerv1.LedgerAccount) *connectorv1.Acco
"allow_negative": account.GetAllowNegative(),
"is_settlement": account.GetIsSettlement(),
})
describable := ledgerAccountDescribable(account)
return &connectorv1.Account{
Ref: &connectorv1.AccountRef{
ConnectorId: ledgerConnectorID,
@@ -353,6 +357,7 @@ func ledgerAccountToConnector(account *ledgerv1.LedgerAccount) *connectorv1.Acco
ProviderDetails: details,
CreatedAt: account.GetCreatedAt(),
UpdatedAt: account.GetUpdatedAt(),
Describable: describable,
}
}
@@ -367,6 +372,71 @@ func ledgerAccountState(status ledgerv1.AccountStatus) connectorv1.AccountState
}
}
func ledgerAccountDescribable(account *ledgerv1.LedgerAccount) *describablev1.Describable {
if account == nil {
return nil
}
if desc := cleanedDescribable(account.GetDescribable()); desc != nil {
return desc
}
metadata := account.GetMetadata()
name := ""
if metadata != nil {
if v := strings.TrimSpace(metadata["name"]); v != "" {
name = v
} else if v := strings.TrimSpace(metadata["label"]); v != "" {
name = v
}
}
if name == "" {
name = strings.TrimSpace(account.GetAccountCode())
}
desc := ""
if metadata != nil {
desc = strings.TrimSpace(metadata["description"])
}
if name == "" && desc == "" {
return nil
}
if desc == "" {
return &describablev1.Describable{Name: name}
}
return &describablev1.Describable{Name: name, Description: &desc}
}
func describableFromLabel(label, description string) *describablev1.Describable {
label = strings.TrimSpace(label)
description = strings.TrimSpace(description)
if label == "" && description == "" {
return nil
}
if description == "" {
return &describablev1.Describable{Name: label}
}
return &describablev1.Describable{Name: label, Description: &description}
}
func cleanedDescribable(desc *describablev1.Describable) *describablev1.Describable {
if desc == nil {
return nil
}
name := strings.TrimSpace(desc.GetName())
var description *string
if desc.Description != nil {
trimmed := strings.TrimSpace(desc.GetDescription())
if trimmed != "" {
description = &trimmed
}
}
if name == "" && description == nil {
return nil
}
return &describablev1.Describable{
Name: name,
Description: description,
}
}
func ledgerReceipt(ref string, status connectorv1.OperationStatus) *connectorv1.OperationReceipt {
return &connectorv1.OperationReceipt{
OperationId: strings.TrimSpace(ref),
@@ -454,7 +524,7 @@ func operationAccountID(party *connectorv1.OperationParty) string {
func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountType, error) {
value, ok := reader.Value(key)
if !ok {
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: account_type is required")
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: account_type is required")
}
switch v := value.(type) {
case string:
@@ -466,7 +536,7 @@ func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountT
case int64:
return ledgerv1.AccountType(v), nil
default:
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: account_type is required")
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: account_type is required")
}
}
@@ -481,7 +551,7 @@ func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) {
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
default:
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: invalid account_type")
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
}
}
@@ -518,15 +588,15 @@ func parseLedgerCharges(reader params.Reader) ([]*ledgerv1.PostingLine, error) {
for i, item := range items {
raw, ok := item.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("charges[%d]: invalid charge entry", i)
return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: invalid charge entry", i))
}
accountRef := strings.TrimSpace(fmt.Sprint(raw["ledger_account_ref"]))
if accountRef == "" {
return nil, fmt.Errorf("charges[%d]: ledger_account_ref is required", i)
return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: ledger_account_ref is required", i))
}
money, err := parseMoneyFromMap(raw)
if err != nil {
return nil, fmt.Errorf("charges[%d]: %w", i, err)
return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("charges[%d]: invalid money", i))
}
lineType := parseLedgerLineType(fmt.Sprint(raw["line_type"]))
result = append(result, &ledgerv1.PostingLine{
@@ -553,12 +623,12 @@ func parseLedgerLineType(value string) ledgerv1.LineType {
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
if raw == nil {
return nil, fmt.Errorf("money is required")
return nil, merrors.InvalidArgument("money is required")
}
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
if amount == "" || currency == "" {
return nil, fmt.Errorf("money is required")
return nil, merrors.InvalidArgument("money is required")
}
return &moneyv1.Money{
Amount: amount,

View File

@@ -9,6 +9,7 @@ import (
type Account struct {
storable.Base `bson:",inline" json:",inline"`
model.PermissionBound `bson:",inline" json:",inline"`
model.Describable `bson:",inline" json:",inline"`
AccountCode string `bson:"accountCode" json:"accountCode"` // e.g., "asset:cash:usd"
Currency string `bson:"currency" json:"currency"` // ISO 4217 currency code

View File

@@ -2,7 +2,6 @@ package notificationimp
import (
"context"
"errors"
"regexp"
"strconv"
"strings"
@@ -75,7 +74,7 @@ func (m *confirmationManager) Stop() {
func (m *confirmationManager) HandleRequest(ctx context.Context, request *model.ConfirmationRequest) error {
if m == nil {
return errors.New("confirmation manager is nil")
return merrors.Internal("confirmation manager is nil")
}
if request == nil {
return merrors.InvalidArgument("confirmation request is nil", "request")
@@ -338,25 +337,25 @@ var currencyPattern = regexp.MustCompile(`^[A-Za-z]{3,10}$`)
func parseConfirmationReply(text string) (*paymenttypes.Money, string, error) {
text = strings.TrimSpace(text)
if text == "" {
return nil, "empty", errors.New("empty reply")
return nil, "empty", merrors.InvalidArgument("empty reply")
}
parts := strings.Fields(text)
if len(parts) < 2 {
if len(parts) == 1 && amountPattern.MatchString(parts[0]) {
return nil, "missing_currency", errors.New("currency is required")
return nil, "missing_currency", merrors.InvalidArgument("currency is required")
}
return nil, "missing_amount", errors.New("amount is required")
return nil, "missing_amount", merrors.InvalidArgument("amount is required")
}
if len(parts) > 2 {
return nil, "format", errors.New("reply format is invalid")
return nil, "format", merrors.InvalidArgument("reply format is invalid")
}
amount := parts[0]
currency := parts[1]
if !amountPattern.MatchString(amount) {
return nil, "invalid_amount", errors.New("amount format is invalid")
return nil, "invalid_amount", merrors.InvalidArgument("amount format is invalid")
}
if !currencyPattern.MatchString(currency) {
return nil, "invalid_currency", errors.New("currency format is invalid")
return nil, "invalid_currency", merrors.InvalidArgument("currency format is invalid")
}
return &paymenttypes.Money{
Amount: amount,

View File

@@ -3,7 +3,6 @@ package discovery
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
@@ -13,6 +12,7 @@ import (
cons "github.com/tech/sendico/pkg/messaging/consumer"
me "github.com/tech/sendico/pkg/messaging/envelope"
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
@@ -29,7 +29,7 @@ type Client struct {
func NewClient(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, sender string) (*Client, error) {
if msgBroker == nil {
return nil, errors.New("discovery client: broker is nil")
return nil, merrors.InvalidArgument("discovery client: broker is nil")
}
if logger == nil {
logger = zap.NewNop()
@@ -82,7 +82,7 @@ func (c *Client) Close() {
func (c *Client) Lookup(ctx context.Context) (LookupResponse, error) {
if c == nil || c.producer == nil {
return LookupResponse{}, errors.New("discovery client: producer not configured")
return LookupResponse{}, merrors.Internal("discovery client: producer not configured")
}
requestID := uuid.NewString()
ch := make(chan LookupResponse, 1)

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/nats-io/nats.go"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
@@ -48,7 +49,7 @@ type KVStore struct {
func NewKVStore(logger mlogger.Logger, js nats.JetStreamContext, bucket string, opts ...KVStoreOption) (*KVStore, error) {
if js == nil {
return nil, errors.New("discovery kv: jetstream is nil")
return nil, merrors.InvalidArgument("discovery kv: jetstream is nil")
}
if logger == nil {
logger = zap.NewNop()
@@ -120,11 +121,11 @@ func ensureKVTTL(logger mlogger.Logger, js nats.JetStreamContext, kv nats.KeyVal
func (s *KVStore) Put(entry RegistryEntry) error {
if s == nil || s.kv == nil {
return errors.New("discovery kv: not configured")
return merrors.Internal("discovery kv: not configured")
}
key := registryEntryKey(normalizeEntry(entry))
if key == "" {
return errors.New("discovery kv: entry key is empty")
return merrors.InvalidArgument("discovery kv: entry key is empty")
}
payload, err := json.Marshal(entry)
if err != nil {
@@ -140,7 +141,7 @@ func (s *KVStore) Put(entry RegistryEntry) error {
func (s *KVStore) Delete(id string) error {
if s == nil || s.kv == nil {
return errors.New("discovery kv: not configured")
return merrors.Internal("discovery kv: not configured")
}
key := kvKeyFromRegistryKey(id)
if key == "" {
@@ -155,7 +156,7 @@ func (s *KVStore) Delete(id string) error {
func (s *KVStore) WatchAll() (nats.KeyWatcher, error) {
if s == nil || s.kv == nil {
return nil, errors.New("discovery kv: not configured")
return nil, merrors.Internal("discovery kv: not configured")
}
return s.kv.WatchAll()
}

View File

@@ -2,9 +2,9 @@ package discovery
import (
"encoding/json"
"errors"
messaging "github.com/tech/sendico/pkg/messaging/envelope"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
)
@@ -15,7 +15,7 @@ type jsonEnvelope struct {
func (e *jsonEnvelope) Serialize() ([]byte, error) {
if e.payload == nil {
return nil, errors.New("discovery envelope payload is nil")
return nil, merrors.InvalidArgument("discovery envelope payload is nil")
}
data, err := json.Marshal(e.payload)
if err != nil {

View File

@@ -3,12 +3,12 @@ package discovery
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go"
"github.com/tech/sendico/pkg/merrors"
msg "github.com/tech/sendico/pkg/messaging"
mb "github.com/tech/sendico/pkg/messaging/broker"
cons "github.com/tech/sendico/pkg/messaging/consumer"
@@ -50,13 +50,13 @@ type consumerHandler struct {
func NewRegistryService(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, registry *Registry, sender string, opts ...RegistryOption) (*RegistryService, error) {
if msgBroker == nil {
return nil, errors.New("discovery registry: broker is nil")
return nil, merrors.InvalidArgument("discovery registry: broker is nil", "broker")
}
if registry == nil {
registry = NewRegistry()
}
if logger == nil {
logger = zap.NewNop()
return nil, merrors.InvalidArgument("discovery registry: no logger provided", "logger")
}
logger = logger.Named("discovery_registry")
sender = strings.TrimSpace(sender)

View File

@@ -2,12 +2,12 @@ package discovery
import (
"encoding/json"
"errors"
"sync"
"time"
"github.com/nats-io/nats.go"
mb "github.com/tech/sendico/pkg/messaging/broker"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
@@ -23,22 +23,22 @@ type RegistryWatcher struct {
func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Registry) (*RegistryWatcher, error) {
if msgBroker == nil {
return nil, errors.New("discovery watcher: broker is nil")
return nil, merrors.InvalidArgument("discovery watcher: broker is nil")
}
if registry == nil {
registry = NewRegistry()
}
if logger == nil {
return nil, errors.New("discovery logger: logger must be provided")
return nil, merrors.InvalidArgument("discovery logger: logger must be provided")
}
logger = logger.Named("discovery_watcher")
provider, ok := msgBroker.(jetStreamProvider)
if !ok {
return nil, errors.New("discovery watcher: jetstream not available")
return nil, merrors.Internal("discovery watcher: jetstream not available")
}
js := provider.JetStream()
if js == nil {
return nil, errors.New("discovery watcher: jetstream not configured")
return nil, merrors.Internal("discovery watcher: jetstream not configured")
}
store, err := NewKVStore(logger, js, "")
if err != nil {
@@ -54,7 +54,7 @@ func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Re
func (w *RegistryWatcher) Start() error {
if w == nil || w.kv == nil {
return errors.New("discovery watcher: not configured")
return merrors.Internal("discovery watcher: not configured")
}
watcher, err := w.kv.WatchAll()
if err != nil {

View File

@@ -6,6 +6,7 @@ option go_package = "github.com/tech/sendico/pkg/proto/connector/v1;connectorv1"
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "common/describable/v1/describable.proto";
import "common/money/v1/money.proto";
import "common/pagination/v1/cursor.proto";
@@ -131,6 +132,7 @@ message Account {
google.protobuf.Struct provider_details = 7;
google.protobuf.Timestamp created_at = 8;
google.protobuf.Timestamp updated_at = 9;
common.describable.v1.Describable describable = 10;
}
message Balance {

View File

@@ -5,6 +5,7 @@ package ledger.v1;
option go_package = "github.com/tech/sendico/pkg/proto/ledger/v1;ledgerv1";
import "google/protobuf/timestamp.proto";
import "common/describable/v1/describable.proto";
import "common/money/v1/money.proto";
// ===== Enums =====
@@ -55,13 +56,14 @@ message LedgerAccount {
map<string, string> metadata = 9;
google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp updated_at = 11;
common.describable.v1.Describable describable = 12;
}
// A single posting line (mirrors your PostingLine model)
message PostingLine {
string ledger_account_ref = 1;
common.money.v1.Money money = 2;
LineType line_type = 3; // MAIN, FEE, SPREAD, ...
string ledger_account_ref = 1;
common.money.v1.Money money = 2;
LineType line_type = 3; // MAIN, FEE, SPREAD, ...
}
// ===== Requests/Responses =====
@@ -75,6 +77,7 @@ message CreateAccountRequest {
bool allow_negative = 6;
bool is_settlement = 7;
map<string, string> metadata = 8;
common.describable.v1.Describable describable = 9;
}
message CreateAccountResponse {
@@ -124,12 +127,12 @@ message FXRequest {
string from_ledger_account_ref = 3;
string to_ledger_account_ref = 4;
common.money.v1.Money from_money = 5; // debited
common.money.v1.Money to_money = 6; // credited
string rate = 7; // quoted rate as string (snapshot for audit)
common.money.v1.Money from_money = 5; // debited
common.money.v1.Money to_money = 6; // credited
string rate = 7; // quoted rate as string (snapshot for audit)
string description = 8;
repeated PostingLine charges = 9; // FEE/SPREAD lines
repeated PostingLine charges = 9; // FEE/SPREAD lines
map<string, string> metadata = 10;
google.protobuf.Timestamp event_time = 11;
}

View File

@@ -4,6 +4,7 @@ import (
"strings"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
)
type LedgerAccountType string
@@ -32,6 +33,7 @@ type CreateLedgerAccount struct {
AllowNegative bool `json:"allowNegative,omitempty"`
IsSettlement bool `json:"isSettlement,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Describable *model.Describable `json:"describable,omitempty"`
}
func (r *CreateLedgerAccount) Validate() error {

View File

@@ -12,6 +12,8 @@ type Signup struct {
Organization model.Describable `json:"organization"`
OrganizationTimeZone string `json:"organizationTimeZone"`
OwnerRole model.Describable `json:"ownerRole"`
CryptoWallet model.Describable `json:"cryptoWallet"`
LedgerWallet model.Describable `json:"ledgerWallet"`
}
// UnmarshalJSON enforces strict parsing to catch malformed or unexpected fields.

View File

@@ -16,6 +16,7 @@ import (
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
@@ -252,6 +253,10 @@ func (a *AccountAPI) openOrgWallet(ctx context.Context, org *model.Organization,
IdempotencyKey: uuid.NewString(),
OrganizationRef: org.ID.Hex(),
OwnerRef: org.ID.Hex(),
Describable: &describablev1.Describable{
Name: sr.CryptoWallet.Name,
Description: sr.CryptoWallet.Description,
},
Asset: a.chainAsset,
Metadata: map[string]string{
"source": "signup",

View File

@@ -15,14 +15,20 @@ import (
)
var (
errConfirmationNotFound = errors.New("confirmation not found or expired")
errConfirmationUsed = errors.New("confirmation already used")
errConfirmationMismatch = errors.New("confirmation code mismatch")
errConfirmationAttemptsExceeded = errors.New("confirmation attempts exceeded")
errConfirmationCooldown = errors.New("confirmation cooldown active")
errConfirmationResendLimit = errors.New("confirmation resend limit reached")
errConfirmationNotFound confirmationError = "confirmation not found or expired"
errConfirmationUsed confirmationError = "confirmation already used"
errConfirmationMismatch confirmationError = "confirmation code mismatch"
errConfirmationAttemptsExceeded confirmationError = "confirmation attempts exceeded"
errConfirmationCooldown confirmationError = "confirmation cooldown active"
errConfirmationResendLimit confirmationError = "confirmation resend limit reached"
)
type confirmationError string
func (e confirmationError) Error() string {
return string(e)
}
type ConfirmationStore struct {
db confirmation.DB
}

View File

@@ -1,11 +1,11 @@
package ledgerapiimp
import (
"errors"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
@@ -24,7 +24,7 @@ func (a *LedgerAPI) getBalance(r *http.Request, account *model.Account, token *s
accountRef := strings.TrimSpace(a.aph.GetID(r))
if accountRef == "" {
return response.BadReference(a.logger, a.Name(), a.aph.Name(), a.aph.GetID(r), errors.New("ledger account reference is required"))
return response.BadReference(a.logger, a.Name(), a.aph.Name(), a.aph.GetID(r), merrors.InvalidArgument("ledger account reference is required"))
}
ctx := r.Context()
@@ -38,7 +38,7 @@ func (a *LedgerAPI) getBalance(r *http.Request, account *model.Account, token *s
return response.AccessDenied(a.logger, a.Name(), "ledger balance read permission denied")
}
if a.client == nil {
return response.Internal(a.logger, mservice.Ledger, errors.New("ledger client is not configured"))
return response.Internal(a.logger, mservice.Ledger, merrors.Internal("ledger client is not configured"))
}
resp, err := a.client.GetBalance(ctx, &ledgerv1.GetBalanceRequest{

View File

@@ -2,7 +2,6 @@ package ledgerapiimp
import (
"encoding/json"
"errors"
"net/http"
"strings"
@@ -10,6 +9,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/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/sresponse"
@@ -52,7 +52,25 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
}
if a.client == nil {
return response.Internal(a.logger, mservice.Ledger, errors.New("ledger client is not configured"))
return response.Internal(a.logger, mservice.Ledger, merrors.Internal("ledger client is not configured"))
}
var describable *describablev1.Describable
if payload.Describable != nil {
name := strings.TrimSpace(payload.Describable.Name)
var description *string
if payload.Describable.Description != nil {
trimmed := strings.TrimSpace(*payload.Describable.Description)
if trimmed != "" {
description = &trimmed
}
}
if name != "" || description != nil {
describable = &describablev1.Describable{
Name: name,
Description: description,
}
}
}
resp, err := a.client.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
@@ -64,6 +82,7 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
AllowNegative: payload.AllowNegative,
IsSettlement: payload.IsSettlement,
Metadata: payload.Metadata,
Describable: describable,
})
if err != nil {
a.logger.Warn("Failed to create ledger account", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
@@ -82,6 +101,20 @@ func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAc
}
payload.AccountCode = strings.TrimSpace(payload.AccountCode)
payload.Currency = strings.ToUpper(strings.TrimSpace(payload.Currency))
if payload.Describable != nil {
payload.Describable.Name = strings.TrimSpace(payload.Describable.Name)
if payload.Describable.Description != nil {
trimmed := strings.TrimSpace(*payload.Describable.Description)
if trimmed == "" {
payload.Describable.Description = nil
} else {
payload.Describable.Description = &trimmed
}
}
if payload.Describable.Name == "" && payload.Describable.Description == nil {
payload.Describable = nil
}
}
if len(payload.Metadata) == 0 {
payload.Metadata = nil
}

View File

@@ -1,10 +1,10 @@
package ledgerapiimp
import (
"errors"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
@@ -32,7 +32,7 @@ func (a *LedgerAPI) listAccounts(r *http.Request, account *model.Account, token
return response.AccessDenied(a.logger, a.Name(), "ledger accounts read permission denied")
}
if a.client == nil {
return response.Internal(a.logger, mservice.Ledger, errors.New("ledger client is not configured"))
return response.Internal(a.logger, mservice.Ledger, merrors.Internal("ledger client is not configured"))
}
resp, err := a.client.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{

View File

@@ -3,13 +3,13 @@ package paymentapiimp
import (
"context"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/discovery"
me "github.com/tech/sendico/pkg/messaging/envelope"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
@@ -21,7 +21,7 @@ const discoveryLookupTimeout = 3 * time.Second
func (a *PaymentAPI) listDiscoveryRegistry(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
if a.discovery == nil {
return response.Internal(a.logger, a.Name(), errors.New("discovery client is not configured"))
return response.Internal(a.logger, a.Name(), merrors.Internal("discovery client is not configured"))
}
orgRef, err := a.oph.GetRef(r)
@@ -55,7 +55,7 @@ func (a *PaymentAPI) listDiscoveryRegistry(r *http.Request, account *model.Accou
func (a *PaymentAPI) getDiscoveryRefresh(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
if a.refreshConsumer == nil {
return response.Internal(a.logger, a.Name(), errors.New("discovery refresh consumer is not configured"))
return response.Internal(a.logger, a.Name(), merrors.Internal("discovery refresh consumer is not configured"))
}
orgRef, err := a.oph.GetRef(r)

View File

@@ -1,11 +1,11 @@
package walletapiimp
import (
"errors"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
@@ -23,7 +23,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
}
walletRef := strings.TrimSpace(a.wph.GetID(r))
if walletRef == "" {
return response.BadReference(a.logger, a.Name(), a.wph.Name(), a.wph.GetID(r), errors.New("wallet reference is required"))
return response.BadReference(a.logger, a.Name(), a.wph.Name(), a.wph.GetID(r), merrors.InvalidArgument("wallet reference is required"))
}
ctx := r.Context()
@@ -37,7 +37,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
return response.AccessDenied(a.logger, a.Name(), "wallet balance read permission denied")
}
if a.chainGateway == nil {
return response.Internal(a.logger, mservice.ChainGateway, errors.New("chain gateway client is not configured"))
return response.Internal(a.logger, mservice.ChainGateway, merrors.Internal("chain gateway client is not configured"))
}
resp, err := a.chainGateway.GetWalletBalance(ctx, &chainv1.GetWalletBalanceRequest{WalletRef: walletRef})
@@ -49,7 +49,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
bal := resp.GetBalance()
if bal == nil {
a.logger.Warn("Wallet balance missing in response", zap.String("wallet_ref", walletRef))
return response.Auto(a.logger, mservice.ChainGateway, errors.New("wallet balance not available"))
return response.Auto(a.logger, mservice.ChainGateway, merrors.Internal("wallet balance not available"))
}
return sresponse.WalletBalance(a.logger, bal, token)

View File

@@ -1,11 +1,11 @@
package walletapiimp
import (
"errors"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
@@ -33,7 +33,7 @@ func (a *WalletAPI) listWallets(r *http.Request, account *model.Account, token *
return response.AccessDenied(a.logger, a.Name(), "wallets read permission denied")
}
if a.chainGateway == nil {
return response.Internal(a.logger, mservice.ChainGateway, errors.New("chain gateway client is not configured"))
return response.Internal(a.logger, mservice.ChainGateway, merrors.Internal("chain gateway client is not configured"))
}
req := &chainv1.ListManagedWalletsRequest{

View File

@@ -12,14 +12,18 @@ part 'signup.g.dart';
class SignupRequest {
final AccountData account;
final DescribableDTO organization;
final String organizationTimeZone;
final DescribableDTO ownerRole;
final DescribableDTO cryptoWallet;
final DescribableDTO ledgerWallet;
final String organizationTimeZone;
const SignupRequest({
required this.account,
required this.organization,
required this.organizationTimeZone,
required this.ownerRole,
required this.cryptoWallet,
required this.ledgerWallet,
});
factory SignupRequest.build({
@@ -27,11 +31,15 @@ class SignupRequest {
required Describable organization,
required String organizationTimeZone,
required Describable ownerRole,
required Describable cryptoWallet,
required Describable ledgerWallet,
}) => SignupRequest(
account: account,
organization: organization.toDTO(),
organizationTimeZone: organizationTimeZone,
ownerRole: ownerRole.toDTO(),
cryptoWallet: cryptoWallet.toDTO(),
ledgerWallet: ledgerWallet.toDTO(),
);
factory SignupRequest.fromJson(Map<String, dynamic> json) => _$SignupRequestFromJson(json);

View File

@@ -165,8 +165,10 @@ class AccountProvider extends ChangeNotifier {
Future<void> signup({
required AccountData account,
required Describable organization,
required String timezone,
required Describable ownerRole,
required Describable cryptoWallet,
required Describable ledgerWallet,
required String timezone,
}) async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
@@ -176,6 +178,8 @@ class AccountProvider extends ChangeNotifier {
organization: organization,
organizationTimeZone: timezone,
ownerRole: ownerRole,
cryptoWallet: cryptoWallet,
ledgerWallet: ledgerWallet,
),
);
// Signup might not automatically log in the user,

View File

@@ -474,6 +474,10 @@
"optional": "optional",
"ownerRole": "Organization Owner",
"ownerRoleDescription": "This role is granted to the organizations creator, providing full administrative privileges",
"cryptoWallet": "Crypto",
"cryptoWalletDesc": "TRC-20 USDT",
"ledgerWallet": "Internal",
"ledgerWalletDesc": "RUB wallet for settlements",
"accountVerificationFailed": "Oops! We failed to verify your account. Please, contact support",
"verifyAccount": "Account Verification",
"verificationFailed": "Verification Failed",

View File

@@ -475,6 +475,10 @@
"ownerRole": "Владелец организации",
"ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права",
"cryptoWallet": "Крипто",
"cryptoWalletDesc": "TRC-20 USDT",
"ledgerWallet": "Внутренний",
"ledgerWalletDesc": "RUB кошелек для расчетов",
"accountVerificationFailed": "Упс! Не удалось подтвердить ваш аккаунт. Пожалуйста, свяжитесь с поддержкой.",
"verifyAccount": "Подтвердить аккаунт",
"verificationFailed": "Ошибка подтверждения",

View File

@@ -71,6 +71,14 @@ class SignUpFormState extends State<SignUpForm> {
name: locs.ownerRole,
description: locs.ownerRoleDescription,
),
cryptoWallet: newDescribable(
name: locs.cryptoWallet,
description: locs.cryptoWalletDesc,
),
ledgerWallet: newDescribable(
name: locs.ledgerWallet,
description: locs.ledgerWalletDesc,
),
);
onSignUp();
return 'ok';