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() == "" { if asset.GetTokenSymbol() == "" {
asset.TokenSymbol = strings.TrimSpace(tokenFromAssetString(account.GetAsset())) 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{ return &chainv1.ManagedWallet{
WalletRef: walletRef, WalletRef: walletRef,
OrganizationRef: organizationRef, OrganizationRef: organizationRef,
@@ -382,9 +396,7 @@ func managedWalletFromAccount(account *connectorv1.Account) *chainv1.ManagedWall
Status: managedWalletStatusFromAccount(account.GetState()), Status: managedWalletStatusFromAccount(account.GetState()),
CreatedAt: account.GetCreatedAt(), CreatedAt: account.GetCreatedAt(),
UpdatedAt: account.GetUpdatedAt(), UpdatedAt: account.GetUpdatedAt(),
Describable: &describablev1.Describable{ Describable: describable,
Name: strings.TrimSpace(account.GetLabel()),
},
} }
} }

View File

@@ -376,6 +376,7 @@ func chainWalletToAccount(wallet *chainv1.ManagedWallet) *connectorv1.Account {
ProviderDetails: details, ProviderDetails: details,
CreatedAt: wallet.GetCreatedAt(), CreatedAt: wallet.GetCreatedAt(),
UpdatedAt: wallet.GetUpdatedAt(), 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) { func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
if op == nil { 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 to := op.GetTo(); to != nil {
if account := to.GetAccount(); account != 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 &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 { 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) { func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
if raw == nil { if raw == nil {
return nil, fmt.Errorf("money is required") return nil, merrors.InvalidArgument("money is required")
} }
amount := strings.TrimSpace(fmt.Sprint(raw["amount"])) amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
currency := strings.TrimSpace(fmt.Sprint(raw["currency"])) currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
if amount == "" || currency == "" { if amount == "" || currency == "" {
return nil, fmt.Errorf("money is required") return nil, merrors.InvalidArgument("money is required")
} }
return &moneyv1.Money{ return &moneyv1.Money{
Amount: amount, Amount: amount,
@@ -575,11 +576,11 @@ func parseChainAsset(assetString string, reader params.Reader) (*chainv1.Asset,
network = networkFromAssetString(assetString) network = networkFromAssetString(assetString)
} }
if token == "" { if token == "" {
return nil, fmt.Errorf("asset: token_symbol is required") return nil, merrors.InvalidArgument("asset: token_symbol is required")
} }
chain := shared.ChainEnumFromName(network) chain := shared.ChainEnumFromName(network)
if chain == chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED { 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{ return &chainv1.Asset{
Chain: chain, Chain: chain,

View File

@@ -1,15 +1,16 @@
package shared package shared
import ( import (
"errors"
"math/big" "math/big"
"strings" "strings"
"github.com/tech/sendico/pkg/merrors"
) )
var ( var (
errHexEmpty = errors.New("hex value is empty") errHexEmpty = merrors.InvalidArgument("hex value is empty")
errHexInvalid = errors.New("invalid hex number") errHexInvalid = merrors.InvalidArgument("invalid hex number")
errHexOutOfRange = errors.New("hex number out of range") errHexOutOfRange = merrors.InvalidArgument("hex number out of range")
) )
// DecodeHexBig parses a hex string that may include leading zero digits. // 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 { func (i *Imp) startHTTPCallbackServer(svc *mntxservice.Service, cfg callbackRuntimeConfig) error {
if svc == nil { 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) == "" { if strings.TrimSpace(cfg.Address) == "" {
i.logger.Info("Monetix callback server disabled: address is empty") i.logger.Info("Monetix callback server disabled: address is empty")

View File

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

View File

@@ -3,7 +3,6 @@ package gateway
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"strings" "strings"
"github.com/tech/sendico/pkg/connector/params" "github.com/tech/sendico/pkg/connector/params"
@@ -139,7 +138,7 @@ func tgsettleOperationParams() []*connectorv1.OperationParamSpec {
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) { func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
if op == nil { 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 to := op.GetTo(); to != nil {
if account := to.GetAccount(); account != 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 &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 { func normalizeMoneyForTransfer(m *moneyv1.Money) *moneyv1.Money {

View File

@@ -2,12 +2,12 @@ package storage
import ( import (
"context" "context"
"errors"
"github.com/tech/sendico/gateway/tgsettle/storage/model" "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 { type Repository interface {
Payments() PaymentsStore Payments() PaymentsStore

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/payments/rail" "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" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/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(), "allow_negative": req.GetAllowNegative(),
"is_settlement": req.GetIsSettlement(), "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 { if len(req.GetMetadata()) > 0 {
params["metadata"] = mapStringToInterface(req.GetMetadata()) params["metadata"] = mapStringToInterface(req.GetMetadata())
} }
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{ resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT, Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
Asset: strings.TrimSpace(req.GetCurrency()), Asset: strings.TrimSpace(req.GetCurrency()),
Label: label,
Params: structFromMap(params), Params: structFromMap(params),
}) })
if err != nil { if err != nil {
@@ -469,6 +481,18 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
if ref := account.GetRef(); ref != nil { if ref := account.GetRef(); ref != nil {
accountID = strings.TrimSpace(ref.GetAccountId()) 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{ return &ledgerv1.LedgerAccount{
LedgerAccountRef: accountID, LedgerAccountRef: accountID,
OrganizationRef: strings.TrimSpace(account.GetOwnerRef()), OrganizationRef: strings.TrimSpace(account.GetOwnerRef()),
@@ -480,6 +504,7 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
IsSettlement: isSettlement, IsSettlement: isSettlement,
CreatedAt: account.GetCreatedAt(), CreatedAt: account.GetCreatedAt(),
UpdatedAt: account.GetUpdatedAt(), UpdatedAt: account.GetUpdatedAt(),
Describable: describable,
} }
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/ledger/storage/model" "github.com/tech/sendico/ledger/storage/model"
"github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors" "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" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
@@ -62,6 +63,8 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
metadata = nil metadata = nil
} }
describable := describableFromProto(req.GetDescribable())
account := &model.Account{ account := &model.Account{
AccountCode: accountCode, AccountCode: accountCode,
Currency: currency, Currency: currency,
@@ -71,6 +74,9 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
IsSettlement: req.GetIsSettlement(), IsSettlement: req.GetIsSettlement(),
Metadata: metadata, Metadata: metadata,
} }
if describable != nil {
account.Describable = *describable
}
account.OrganizationRef = orgRef account.OrganizationRef = orgRef
err = s.storage.Accounts().Create(ctx, account) err = s.storage.Accounts().Create(ctx, account)
@@ -204,5 +210,45 @@ func toProtoAccount(account *model.Account) *ledgerv1.LedgerAccount {
Metadata: metadata, Metadata: metadata,
CreatedAt: createdAt, CreatedAt: createdAt,
UpdatedAt: updatedAt, 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/ledger/internal/appversion"
"github.com/tech/sendico/pkg/connector/params" "github.com/tech/sendico/pkg/connector/params"
"github.com/tech/sendico/pkg/merrors" "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" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/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") status := parseLedgerAccountStatus(reader, "status")
metadata := mergeMetadata(reader.StringMap("metadata"), req.GetLabel(), req.GetOwnerRef(), req.GetCorrelationId(), req.GetParentIntentId()) 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{ resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
OrganizationRef: orgRef, OrganizationRef: orgRef,
@@ -86,6 +88,7 @@ func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.Ope
AllowNegative: reader.Bool("allow_negative"), AllowNegative: reader.Bool("allow_negative"),
IsSettlement: reader.Bool("is_settlement"), IsSettlement: reader.Bool("is_settlement"),
Metadata: metadata, Metadata: metadata,
Describable: describable,
}) })
if err != nil { if err != nil {
return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, 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(), "allow_negative": account.GetAllowNegative(),
"is_settlement": account.GetIsSettlement(), "is_settlement": account.GetIsSettlement(),
}) })
describable := ledgerAccountDescribable(account)
return &connectorv1.Account{ return &connectorv1.Account{
Ref: &connectorv1.AccountRef{ Ref: &connectorv1.AccountRef{
ConnectorId: ledgerConnectorID, ConnectorId: ledgerConnectorID,
@@ -353,6 +357,7 @@ func ledgerAccountToConnector(account *ledgerv1.LedgerAccount) *connectorv1.Acco
ProviderDetails: details, ProviderDetails: details,
CreatedAt: account.GetCreatedAt(), CreatedAt: account.GetCreatedAt(),
UpdatedAt: account.GetUpdatedAt(), 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 { func ledgerReceipt(ref string, status connectorv1.OperationStatus) *connectorv1.OperationReceipt {
return &connectorv1.OperationReceipt{ return &connectorv1.OperationReceipt{
OperationId: strings.TrimSpace(ref), OperationId: strings.TrimSpace(ref),
@@ -454,7 +524,7 @@ func operationAccountID(party *connectorv1.OperationParty) string {
func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountType, error) { func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountType, error) {
value, ok := reader.Value(key) value, ok := reader.Value(key)
if !ok { 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) { switch v := value.(type) {
case string: case string:
@@ -466,7 +536,7 @@ func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountT
case int64: case int64:
return ledgerv1.AccountType(v), nil return ledgerv1.AccountType(v), nil
default: 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": case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
default: 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 { for i, item := range items {
raw, ok := item.(map[string]interface{}) raw, ok := item.(map[string]interface{})
if !ok { 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"])) accountRef := strings.TrimSpace(fmt.Sprint(raw["ledger_account_ref"]))
if accountRef == "" { 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) money, err := parseMoneyFromMap(raw)
if err != nil { 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"])) lineType := parseLedgerLineType(fmt.Sprint(raw["line_type"]))
result = append(result, &ledgerv1.PostingLine{ result = append(result, &ledgerv1.PostingLine{
@@ -553,12 +623,12 @@ func parseLedgerLineType(value string) ledgerv1.LineType {
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) { func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
if raw == nil { if raw == nil {
return nil, fmt.Errorf("money is required") return nil, merrors.InvalidArgument("money is required")
} }
amount := strings.TrimSpace(fmt.Sprint(raw["amount"])) amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
currency := strings.TrimSpace(fmt.Sprint(raw["currency"])) currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
if amount == "" || currency == "" { if amount == "" || currency == "" {
return nil, fmt.Errorf("money is required") return nil, merrors.InvalidArgument("money is required")
} }
return &moneyv1.Money{ return &moneyv1.Money{
Amount: amount, Amount: amount,

View File

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

View File

@@ -2,7 +2,6 @@ package notificationimp
import ( import (
"context" "context"
"errors"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -75,7 +74,7 @@ func (m *confirmationManager) Stop() {
func (m *confirmationManager) HandleRequest(ctx context.Context, request *model.ConfirmationRequest) error { func (m *confirmationManager) HandleRequest(ctx context.Context, request *model.ConfirmationRequest) error {
if m == nil { if m == nil {
return errors.New("confirmation manager is nil") return merrors.Internal("confirmation manager is nil")
} }
if request == nil { if request == nil {
return merrors.InvalidArgument("confirmation request is nil", "request") 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) { func parseConfirmationReply(text string) (*paymenttypes.Money, string, error) {
text = strings.TrimSpace(text) text = strings.TrimSpace(text)
if text == "" { if text == "" {
return nil, "empty", errors.New("empty reply") return nil, "empty", merrors.InvalidArgument("empty reply")
} }
parts := strings.Fields(text) parts := strings.Fields(text)
if len(parts) < 2 { if len(parts) < 2 {
if len(parts) == 1 && amountPattern.MatchString(parts[0]) { 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 { 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] amount := parts[0]
currency := parts[1] currency := parts[1]
if !amountPattern.MatchString(amount) { 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) { 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{ return &paymenttypes.Money{
Amount: amount, Amount: amount,

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,12 @@ package discovery
import ( import (
"encoding/json" "encoding/json"
"errors"
"sync" "sync"
"time" "time"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
mb "github.com/tech/sendico/pkg/messaging/broker" mb "github.com/tech/sendico/pkg/messaging/broker"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -23,22 +23,22 @@ type RegistryWatcher struct {
func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Registry) (*RegistryWatcher, error) { func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Registry) (*RegistryWatcher, error) {
if msgBroker == nil { if msgBroker == nil {
return nil, errors.New("discovery watcher: broker is nil") return nil, merrors.InvalidArgument("discovery watcher: broker is nil")
} }
if registry == nil { if registry == nil {
registry = NewRegistry() registry = NewRegistry()
} }
if logger == nil { 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") logger = logger.Named("discovery_watcher")
provider, ok := msgBroker.(jetStreamProvider) provider, ok := msgBroker.(jetStreamProvider)
if !ok { if !ok {
return nil, errors.New("discovery watcher: jetstream not available") return nil, merrors.Internal("discovery watcher: jetstream not available")
} }
js := provider.JetStream() js := provider.JetStream()
if js == nil { 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, "") store, err := NewKVStore(logger, js, "")
if err != nil { if err != nil {
@@ -54,7 +54,7 @@ func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Re
func (w *RegistryWatcher) Start() error { func (w *RegistryWatcher) Start() error {
if w == nil || w.kv == nil { 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() watcher, err := w.kv.WatchAll()
if err != nil { 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/struct.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
import "common/describable/v1/describable.proto";
import "common/money/v1/money.proto"; import "common/money/v1/money.proto";
import "common/pagination/v1/cursor.proto"; import "common/pagination/v1/cursor.proto";
@@ -131,6 +132,7 @@ message Account {
google.protobuf.Struct provider_details = 7; google.protobuf.Struct provider_details = 7;
google.protobuf.Timestamp created_at = 8; google.protobuf.Timestamp created_at = 8;
google.protobuf.Timestamp updated_at = 9; google.protobuf.Timestamp updated_at = 9;
common.describable.v1.Describable describable = 10;
} }
message Balance { message Balance {

View File

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

View File

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

View File

@@ -12,6 +12,8 @@ type Signup struct {
Organization model.Describable `json:"organization"` Organization model.Describable `json:"organization"`
OrganizationTimeZone string `json:"organizationTimeZone"` OrganizationTimeZone string `json:"organizationTimeZone"`
OwnerRole model.Describable `json:"ownerRole"` 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. // 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/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap" "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" 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/srequest"
"github.com/tech/sendico/server/interface/api/sresponse" "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(), IdempotencyKey: uuid.NewString(),
OrganizationRef: org.ID.Hex(), OrganizationRef: org.ID.Hex(),
OwnerRef: org.ID.Hex(), OwnerRef: org.ID.Hex(),
Describable: &describablev1.Describable{
Name: sr.CryptoWallet.Name,
Description: sr.CryptoWallet.Description,
},
Asset: a.chainAsset, Asset: a.chainAsset,
Metadata: map[string]string{ Metadata: map[string]string{
"source": "signup", "source": "signup",

View File

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

View File

@@ -1,11 +1,11 @@
package ledgerapiimp package ledgerapiimp
import ( import (
"errors"
"net/http" "net/http"
"strings" "strings"
"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/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" 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)) accountRef := strings.TrimSpace(a.aph.GetID(r))
if accountRef == "" { 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() 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") return response.AccessDenied(a.logger, a.Name(), "ledger balance read permission denied")
} }
if a.client == nil { 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{ resp, err := a.client.GetBalance(ctx, &ledgerv1.GetBalanceRequest{

View File

@@ -2,7 +2,6 @@ package ledgerapiimp
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"strings" "strings"
@@ -10,6 +9,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"
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"
"github.com/tech/sendico/server/interface/api/sresponse" "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 { 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{ 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, AllowNegative: payload.AllowNegative,
IsSettlement: payload.IsSettlement, IsSettlement: payload.IsSettlement,
Metadata: payload.Metadata, Metadata: payload.Metadata,
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), zap.String("organization_ref", orgRef.Hex()))
@@ -82,6 +101,20 @@ func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAc
} }
payload.AccountCode = strings.TrimSpace(payload.AccountCode) payload.AccountCode = strings.TrimSpace(payload.AccountCode)
payload.Currency = strings.ToUpper(strings.TrimSpace(payload.Currency)) 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 { if len(payload.Metadata) == 0 {
payload.Metadata = nil payload.Metadata = nil
} }

View File

@@ -1,10 +1,10 @@
package ledgerapiimp package ledgerapiimp
import ( import (
"errors"
"net/http" "net/http"
"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/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" 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") return response.AccessDenied(a.logger, a.Name(), "ledger accounts read permission denied")
} }
if a.client == nil { 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{ resp, err := a.client.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{

View File

@@ -3,13 +3,13 @@ package paymentapiimp
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"time" "time"
"github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/pkg/discovery"
me "github.com/tech/sendico/pkg/messaging/envelope" me "github.com/tech/sendico/pkg/messaging/envelope"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"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"
@@ -21,7 +21,7 @@ const discoveryLookupTimeout = 3 * time.Second
func (a *PaymentAPI) listDiscoveryRegistry(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc { func (a *PaymentAPI) listDiscoveryRegistry(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
if a.discovery == nil { 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) 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 { func (a *PaymentAPI) getDiscoveryRefresh(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
if a.refreshConsumer == nil { 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) orgRef, err := a.oph.GetRef(r)

View File

@@ -1,11 +1,11 @@
package walletapiimp package walletapiimp
import ( import (
"errors"
"net/http" "net/http"
"strings" "strings"
"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/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" 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)) walletRef := strings.TrimSpace(a.wph.GetID(r))
if walletRef == "" { 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() 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") return response.AccessDenied(a.logger, a.Name(), "wallet balance read permission denied")
} }
if a.chainGateway == nil { 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}) 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() bal := resp.GetBalance()
if bal == nil { if bal == nil {
a.logger.Warn("Wallet balance missing in response", zap.String("wallet_ref", walletRef)) 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) return sresponse.WalletBalance(a.logger, bal, token)

View File

@@ -1,11 +1,11 @@
package walletapiimp package walletapiimp
import ( import (
"errors"
"net/http" "net/http"
"strings" "strings"
"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/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" 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") return response.AccessDenied(a.logger, a.Name(), "wallets read permission denied")
} }
if a.chainGateway == nil { 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{ req := &chainv1.ListManagedWalletsRequest{

View File

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

View File

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

View File

@@ -474,6 +474,10 @@
"optional": "optional", "optional": "optional",
"ownerRole": "Organization Owner", "ownerRole": "Organization Owner",
"ownerRoleDescription": "This role is granted to the organizations creator, providing full administrative privileges", "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", "accountVerificationFailed": "Oops! We failed to verify your account. Please, contact support",
"verifyAccount": "Account Verification", "verifyAccount": "Account Verification",
"verificationFailed": "Verification Failed", "verificationFailed": "Verification Failed",

View File

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

View File

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