From 43edbc109d817ca6a5b6a06457b3202befd5a850 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 6 Jan 2026 17:51:35 +0100 Subject: [PATCH] ledger account describibale support --- api/gateway/chain/client/client.go | 18 +++- .../internal/service/gateway/connector.go | 13 +-- .../internal/service/gateway/shared/hex.go | 9 +- .../internal/server/internal/serverimp.go | 2 +- .../internal/service/gateway/connector.go | 11 ++- .../internal/service/gateway/connector.go | 5 +- api/gateway/tgsettle/storage/storage.go | 4 +- api/ledger/client/client.go | 25 ++++++ .../internal/service/ledger/accounts.go | 46 ++++++++++ .../internal/service/ledger/connector.go | 86 +++++++++++++++++-- api/ledger/storage/model/account.go | 1 + .../server/notificationimp/confirmation.go | 15 ++-- api/pkg/discovery/client.go | 6 +- api/pkg/discovery/kv.go | 11 +-- api/pkg/discovery/messages.go | 4 +- api/pkg/discovery/service.go | 6 +- api/pkg/discovery/watcher.go | 12 +-- api/proto/connector/v1/connector.proto | 2 + api/proto/ledger/v1/ledger.proto | 17 ++-- api/server/interface/api/srequest/ledger.go | 2 + api/server/interface/api/srequest/signup.go | 2 + .../internal/server/accountapiimp/signup.go | 5 ++ .../internal/server/confirmationimp/store.go | 18 ++-- .../internal/server/ledgerapiimp/balance.go | 6 +- .../internal/server/ledgerapiimp/create.go | 37 +++++++- .../internal/server/ledgerapiimp/list.go | 4 +- .../server/paymentapiimp/discovery.go | 6 +- .../internal/server/walletapiimp/balance.go | 8 +- .../internal/server/walletapiimp/list.go | 4 +- frontend/pshared/lib/api/requests/signup.dart | 10 ++- frontend/pshared/lib/provider/account.dart | 6 +- frontend/pweb/lib/l10n/en.arb | 4 + frontend/pweb/lib/l10n/ru.arb | 4 + .../pweb/lib/pages/signup/form/state.dart | 8 ++ 34 files changed, 326 insertions(+), 91 deletions(-) diff --git a/api/gateway/chain/client/client.go b/api/gateway/chain/client/client.go index dd87230..12d5a55 100644 --- a/api/gateway/chain/client/client.go +++ b/api/gateway/chain/client/client.go @@ -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, } } diff --git a/api/gateway/chain/internal/service/gateway/connector.go b/api/gateway/chain/internal/service/gateway/connector.go index 93f2e6a..3dbfd36 100644 --- a/api/gateway/chain/internal/service/gateway/connector.go +++ b/api/gateway/chain/internal/service/gateway/connector.go @@ -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, diff --git a/api/gateway/chain/internal/service/gateway/shared/hex.go b/api/gateway/chain/internal/service/gateway/shared/hex.go index da1fc54..e33ac58 100644 --- a/api/gateway/chain/internal/service/gateway/shared/hex.go +++ b/api/gateway/chain/internal/service/gateway/shared/hex.go @@ -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. diff --git a/api/gateway/mntx/internal/server/internal/serverimp.go b/api/gateway/mntx/internal/server/internal/serverimp.go index 1a015f4..14ccf52 100644 --- a/api/gateway/mntx/internal/server/internal/serverimp.go +++ b/api/gateway/mntx/internal/server/internal/serverimp.go @@ -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") diff --git a/api/gateway/mntx/internal/service/gateway/connector.go b/api/gateway/mntx/internal/service/gateway/connector.go index e17fb19..3024337 100644 --- a/api/gateway/mntx/internal/service/gateway/connector.go +++ b/api/gateway/mntx/internal/service/gateway/connector.go @@ -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 diff --git a/api/gateway/tgsettle/internal/service/gateway/connector.go b/api/gateway/tgsettle/internal/service/gateway/connector.go index 287f521..8fd45f6 100644 --- a/api/gateway/tgsettle/internal/service/gateway/connector.go +++ b/api/gateway/tgsettle/internal/service/gateway/connector.go @@ -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 { diff --git a/api/gateway/tgsettle/storage/storage.go b/api/gateway/tgsettle/storage/storage.go index 598bbee..38741d1 100644 --- a/api/gateway/tgsettle/storage/storage.go +++ b/api/gateway/tgsettle/storage/storage.go @@ -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 diff --git a/api/ledger/client/client.go b/api/ledger/client/client.go index 18bc390..a68f51f 100644 --- a/api/ledger/client/client.go +++ b/api/ledger/client/client.go @@ -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, } } diff --git a/api/ledger/internal/service/ledger/accounts.go b/api/ledger/internal/service/ledger/accounts.go index 317179b..ca0d2ad 100644 --- a/api/ledger/internal/service/ledger/accounts.go +++ b/api/ledger/internal/service/ledger/accounts.go @@ -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, } } diff --git a/api/ledger/internal/service/ledger/connector.go b/api/ledger/internal/service/ledger/connector.go index 3e0df7e..570eab3 100644 --- a/api/ledger/internal/service/ledger/connector.go +++ b/api/ledger/internal/service/ledger/connector.go @@ -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, diff --git a/api/ledger/storage/model/account.go b/api/ledger/storage/model/account.go index 27a72f1..d563803 100644 --- a/api/ledger/storage/model/account.go +++ b/api/ledger/storage/model/account.go @@ -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 diff --git a/api/notification/internal/server/notificationimp/confirmation.go b/api/notification/internal/server/notificationimp/confirmation.go index f3e8782..a25971d 100644 --- a/api/notification/internal/server/notificationimp/confirmation.go +++ b/api/notification/internal/server/notificationimp/confirmation.go @@ -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, diff --git a/api/pkg/discovery/client.go b/api/pkg/discovery/client.go index d3cb380..28bfc0d 100644 --- a/api/pkg/discovery/client.go +++ b/api/pkg/discovery/client.go @@ -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) diff --git a/api/pkg/discovery/kv.go b/api/pkg/discovery/kv.go index 650e17a..fd89450 100644 --- a/api/pkg/discovery/kv.go +++ b/api/pkg/discovery/kv.go @@ -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() } diff --git a/api/pkg/discovery/messages.go b/api/pkg/discovery/messages.go index 7acb5e4..e062737 100644 --- a/api/pkg/discovery/messages.go +++ b/api/pkg/discovery/messages.go @@ -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 { diff --git a/api/pkg/discovery/service.go b/api/pkg/discovery/service.go index 99525a5..6f5eccc 100644 --- a/api/pkg/discovery/service.go +++ b/api/pkg/discovery/service.go @@ -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) diff --git a/api/pkg/discovery/watcher.go b/api/pkg/discovery/watcher.go index 0b84acf..adf21ce 100644 --- a/api/pkg/discovery/watcher.go +++ b/api/pkg/discovery/watcher.go @@ -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 { diff --git a/api/proto/connector/v1/connector.proto b/api/proto/connector/v1/connector.proto index 7248f70..98deb15 100644 --- a/api/proto/connector/v1/connector.proto +++ b/api/proto/connector/v1/connector.proto @@ -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 { diff --git a/api/proto/ledger/v1/ledger.proto b/api/proto/ledger/v1/ledger.proto index e6fe38a..b848989 100644 --- a/api/proto/ledger/v1/ledger.proto +++ b/api/proto/ledger/v1/ledger.proto @@ -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 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 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 metadata = 10; google.protobuf.Timestamp event_time = 11; } diff --git a/api/server/interface/api/srequest/ledger.go b/api/server/interface/api/srequest/ledger.go index 1270a50..4aee144 100644 --- a/api/server/interface/api/srequest/ledger.go +++ b/api/server/interface/api/srequest/ledger.go @@ -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 { diff --git a/api/server/interface/api/srequest/signup.go b/api/server/interface/api/srequest/signup.go index c1b7137..4447717 100644 --- a/api/server/interface/api/srequest/signup.go +++ b/api/server/interface/api/srequest/signup.go @@ -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. diff --git a/api/server/internal/server/accountapiimp/signup.go b/api/server/internal/server/accountapiimp/signup.go index 78f0308..2f707cf 100644 --- a/api/server/internal/server/accountapiimp/signup.go +++ b/api/server/internal/server/accountapiimp/signup.go @@ -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", diff --git a/api/server/internal/server/confirmationimp/store.go b/api/server/internal/server/confirmationimp/store.go index 8046329..fb2d4e7 100644 --- a/api/server/internal/server/confirmationimp/store.go +++ b/api/server/internal/server/confirmationimp/store.go @@ -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 } diff --git a/api/server/internal/server/ledgerapiimp/balance.go b/api/server/internal/server/ledgerapiimp/balance.go index 2a48590..660fc11 100644 --- a/api/server/internal/server/ledgerapiimp/balance.go +++ b/api/server/internal/server/ledgerapiimp/balance.go @@ -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{ diff --git a/api/server/internal/server/ledgerapiimp/create.go b/api/server/internal/server/ledgerapiimp/create.go index 8e4ab6d..d546b68 100644 --- a/api/server/internal/server/ledgerapiimp/create.go +++ b/api/server/internal/server/ledgerapiimp/create.go @@ -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 } diff --git a/api/server/internal/server/ledgerapiimp/list.go b/api/server/internal/server/ledgerapiimp/list.go index 2f2c628..1f735b9 100644 --- a/api/server/internal/server/ledgerapiimp/list.go +++ b/api/server/internal/server/ledgerapiimp/list.go @@ -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{ diff --git a/api/server/internal/server/paymentapiimp/discovery.go b/api/server/internal/server/paymentapiimp/discovery.go index 0fe0100..d58c717 100644 --- a/api/server/internal/server/paymentapiimp/discovery.go +++ b/api/server/internal/server/paymentapiimp/discovery.go @@ -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) diff --git a/api/server/internal/server/walletapiimp/balance.go b/api/server/internal/server/walletapiimp/balance.go index aeb05a8..1e8c780 100644 --- a/api/server/internal/server/walletapiimp/balance.go +++ b/api/server/internal/server/walletapiimp/balance.go @@ -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) diff --git a/api/server/internal/server/walletapiimp/list.go b/api/server/internal/server/walletapiimp/list.go index 18aa62f..52a9538 100644 --- a/api/server/internal/server/walletapiimp/list.go +++ b/api/server/internal/server/walletapiimp/list.go @@ -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{ diff --git a/frontend/pshared/lib/api/requests/signup.dart b/frontend/pshared/lib/api/requests/signup.dart index 64d5c2a..db5190a 100644 --- a/frontend/pshared/lib/api/requests/signup.dart +++ b/frontend/pshared/lib/api/requests/signup.dart @@ -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 json) => _$SignupRequestFromJson(json); diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart index 426693b..9b88dce 100644 --- a/frontend/pshared/lib/provider/account.dart +++ b/frontend/pshared/lib/provider/account.dart @@ -165,8 +165,10 @@ class AccountProvider extends ChangeNotifier { Future 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, diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 24b7110..91d4d02 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -474,6 +474,10 @@ "optional": "optional", "ownerRole": "Organization Owner", "ownerRoleDescription": "This role is granted to the organization’s 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", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 8ea6dcd..a53bf96 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -475,6 +475,10 @@ "ownerRole": "Владелец организации", "ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права", + "cryptoWallet": "Крипто", + "cryptoWalletDesc": "TRC-20 USDT", + "ledgerWallet": "Внутренний", + "ledgerWalletDesc": "RUB кошелек для расчетов", "accountVerificationFailed": "Упс! Не удалось подтвердить ваш аккаунт. Пожалуйста, свяжитесь с поддержкой.", "verifyAccount": "Подтвердить аккаунт", "verificationFailed": "Ошибка подтверждения", diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index 83bf1d1..27d3b87 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -71,6 +71,14 @@ class SignUpFormState extends State { 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';