Remove platform tenancy from SXCHNGE customer BFF (#34)
This commit is contained in:
1 parent
7062672a87
commit
981e84a4b4
244 files changed
+651
-17130
No files matched your search
@@ -3,7 +3,6 @@ package model
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -21,14 +20,8 @@ type AccountBase struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ArchivableBase `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
LastName string `bson:"lastName" json:"lastName"`
|
||||
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
|
||||
Metadata *AccountMetadata `bson:"metadata,omitempty" json:"-"`
|
||||
|
||||
// INN is the account owner's taxpayer identifier. It is the canonical source
|
||||
// of the INN for personal (account-owned) payouts, flowing into
|
||||
// customer.tax_identifier.
|
||||
INN string `bson:"inn,omitempty" json:"inn,omitempty"`
|
||||
LastName string `bson:"lastName" json:"lastName"`
|
||||
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
|
||||
}
|
||||
|
||||
func (*AccountBase) Collection() string {
|
||||
@@ -51,7 +44,6 @@ func (a *Account) Copy() *Account {
|
||||
return nil
|
||||
}
|
||||
copied := *a
|
||||
copied.Metadata = a.Metadata.Copy()
|
||||
return &copied
|
||||
}
|
||||
|
||||
@@ -74,37 +66,4 @@ func (a *Account) MatchPassword(password string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func AnonymousUserName(orgRef bson.ObjectID) string {
|
||||
return "anonymous@" + orgRef.Hex()
|
||||
}
|
||||
|
||||
func AccountIsAnonymous(account *UserDataBase, orgRef bson.ObjectID) bool {
|
||||
if account == nil {
|
||||
return false
|
||||
}
|
||||
return AnonymousUserName(orgRef) == account.Login
|
||||
}
|
||||
|
||||
type AccountBound interface {
|
||||
GetAccountRef() bson.ObjectID
|
||||
}
|
||||
|
||||
type AccountBoundStorable interface {
|
||||
storable.Storable
|
||||
OrganizationBound
|
||||
GetAccountRef() *bson.ObjectID
|
||||
}
|
||||
|
||||
const (
|
||||
AccountRefField = "accountRef"
|
||||
)
|
||||
|
||||
type AccountBoundBase struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||
AccountRef *bson.ObjectID `bson:"accountRef,omitempty" json:"accountRef,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AccountBoundBase) GetAccountRef() *bson.ObjectID {
|
||||
return a.AccountRef
|
||||
}
|
||||
const AccountRefField = "accountRef"
|
||||
@@ -1,45 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type AccountMetadata struct {
|
||||
PersonalLedgerProvisioning *LedgerProvisioningMetadata `bson:"personalLedgerProvisioning,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
type LedgerProvisioningMetadata struct {
|
||||
Source string `bson:"source,omitempty" json:"-"`
|
||||
InvitationRef string `bson:"invitationRef,omitempty" json:"-"`
|
||||
TargetCurrencies []string `bson:"targetCurrencies,omitempty" json:"-"`
|
||||
Created []string `bson:"created,omitempty" json:"-"`
|
||||
AlreadyExisting []string `bson:"alreadyExisting,omitempty" json:"-"`
|
||||
Failed []LedgerProvisioningFailure `bson:"failed,omitempty" json:"-"`
|
||||
Outcome string `bson:"outcome,omitempty" json:"-"`
|
||||
LastErrorClass string `bson:"lastErrorClass,omitempty" json:"-"`
|
||||
LastAttemptAt time.Time `bson:"lastAttemptAt,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
type LedgerProvisioningFailure struct {
|
||||
Currency string `bson:"currency,omitempty" json:"-"`
|
||||
ErrorClass string `bson:"errorClass,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
func (m *AccountMetadata) Copy() *AccountMetadata {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
copied := *m
|
||||
copied.PersonalLedgerProvisioning = m.PersonalLedgerProvisioning.Copy()
|
||||
return &copied
|
||||
}
|
||||
|
||||
func (m *LedgerProvisioningMetadata) Copy() *LedgerProvisioningMetadata {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
copied := *m
|
||||
copied.TargetCurrencies = append([]string(nil), m.TargetCurrencies...)
|
||||
copied.Created = append([]string(nil), m.Created...)
|
||||
copied.AlreadyExisting = append([]string(nil), m.AlreadyExisting...)
|
||||
copied.Failed = append([]LedgerProvisioningFailure(nil), m.Failed...)
|
||||
return &copied
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package account_role
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AccountRole string
|
||||
|
||||
const (
|
||||
AccountRoleOperating AccountRole = "operating"
|
||||
AccountRoleHold AccountRole = "hold"
|
||||
AccountRoleTransit AccountRole = "transit"
|
||||
AccountRoleSettlement AccountRole = "settlement"
|
||||
AccountRoleClearing AccountRole = "clearing"
|
||||
AccountRolePending AccountRole = "pending"
|
||||
AccountRoleReserve AccountRole = "reserve"
|
||||
AccountRoleLiquidity AccountRole = "liquidity"
|
||||
AccountRoleFee AccountRole = "fee"
|
||||
AccountRoleChargeback AccountRole = "chargeback"
|
||||
AccountRoleAdjustment AccountRole = "adjustment"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataKeyFromRole = "from_role"
|
||||
MetadataKeyToRole = "to_role"
|
||||
)
|
||||
|
||||
func Parse(value string) (AccountRole, bool) {
|
||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||
case "ACCOUNT_ROLE_OPERATING", "OPERATING":
|
||||
return AccountRoleOperating, true
|
||||
case "ACCOUNT_ROLE_HOLD", "HOLD":
|
||||
return AccountRoleHold, true
|
||||
case "ACCOUNT_ROLE_TRANSIT", "TRANSIT":
|
||||
return AccountRoleTransit, true
|
||||
case "ACCOUNT_ROLE_SETTLEMENT", "SETTLEMENT":
|
||||
return AccountRoleSettlement, true
|
||||
case "ACCOUNT_ROLE_CLEARING", "CLEARING":
|
||||
return AccountRoleClearing, true
|
||||
case "ACCOUNT_ROLE_PENDING", "PENDING":
|
||||
return AccountRolePending, true
|
||||
case "ACCOUNT_ROLE_RESERVE", "RESERVE":
|
||||
return AccountRoleReserve, true
|
||||
case "ACCOUNT_ROLE_LIQUIDITY", "LIQUIDITY":
|
||||
return AccountRoleLiquidity, true
|
||||
case "ACCOUNT_ROLE_FEE", "FEE":
|
||||
return AccountRoleFee, true
|
||||
case "ACCOUNT_ROLE_CHARGEBACK", "CHARGEBACK":
|
||||
return AccountRoleChargeback, true
|
||||
case "ACCOUNT_ROLE_ADJUSTMENT", "ADJUSTMENT":
|
||||
return AccountRoleAdjustment, true
|
||||
case "ACCOUNT_ROLE_UNSPECIFIED", "UNSPECIFIED", "":
|
||||
return "", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAccountCopyDeepCopiesMetadata(t *testing.T) {
|
||||
account := &Account{
|
||||
AccountPublic: AccountPublic{
|
||||
AccountBase: AccountBase{
|
||||
Metadata: &AccountMetadata{
|
||||
PersonalLedgerProvisioning: &LedgerProvisioningMetadata{
|
||||
Source: "invitation",
|
||||
InvitationRef: "invitation-ref",
|
||||
TargetCurrencies: []string{"RUB", "USDT"},
|
||||
Created: []string{"RUB"},
|
||||
AlreadyExisting: []string{"USDT"},
|
||||
Failed: []LedgerProvisioningFailure{{Currency: "EUR", ErrorClass: "timeout"}},
|
||||
Outcome: "partial_failure",
|
||||
LastErrorClass: "timeout",
|
||||
LastAttemptAt: time.Now().UTC(),
|
||||
},
|
||||
},
|
||||
},
|
||||
UserDataBase: UserDataBase{Login: "invitee@example.com"},
|
||||
},
|
||||
Password: "secret",
|
||||
Status: AccountActive,
|
||||
}
|
||||
|
||||
copied := account.Copy()
|
||||
copied.Metadata.PersonalLedgerProvisioning.TargetCurrencies[0] = "EUR"
|
||||
copied.Metadata.PersonalLedgerProvisioning.Created[0] = "EUR"
|
||||
copied.Metadata.PersonalLedgerProvisioning.AlreadyExisting[0] = "EUR"
|
||||
copied.Metadata.PersonalLedgerProvisioning.Failed[0].ErrorClass = "ledger_unavailable"
|
||||
|
||||
metadata := account.Metadata.PersonalLedgerProvisioning
|
||||
if metadata.TargetCurrencies[0] != "RUB" {
|
||||
t.Fatalf("expected original target currency to remain RUB, got %q", metadata.TargetCurrencies[0])
|
||||
}
|
||||
if metadata.Created[0] != "RUB" {
|
||||
t.Fatalf("expected original created currency to remain RUB, got %q", metadata.Created[0])
|
||||
}
|
||||
if metadata.AlreadyExisting[0] != "USDT" {
|
||||
t.Fatalf("expected original existing currency to remain USDT, got %q", metadata.AlreadyExisting[0])
|
||||
}
|
||||
if metadata.Failed[0].ErrorClass != "timeout" {
|
||||
t.Fatalf("expected original failure class to remain timeout, got %q", metadata.Failed[0].ErrorClass)
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// Action represents a permissible action on a resource.
|
||||
type Action string
|
||||
|
||||
// Common actions for resources.
|
||||
const (
|
||||
ActionCreate Action = "create" // Create a resource
|
||||
ActionRead Action = "read" // Read or view a resource
|
||||
ActionUpdate Action = "update" // Update or modify a resource
|
||||
ActionDelete Action = "delete" // Delete a resource
|
||||
)
|
||||
|
||||
// Effect determines whether an action is allowed or denied.
|
||||
type Effect string
|
||||
|
||||
const (
|
||||
EffectAllow Effect = "allow" // Permit the action
|
||||
EffectDeny Effect = "deny" // Deny the action
|
||||
)
|
||||
|
||||
// RoleDescription provides metadata about a role.
|
||||
type RoleDescription struct {
|
||||
storable.Base `bson:",inline" json:",inline"` // Base fields for MongoDB documents
|
||||
Describable `bson:",inline" json:",inline"` // Name and description fields
|
||||
OrganizationRef bson.ObjectID `bson:"organizationRef" json:"organizationRef"` // Organization associated with the role
|
||||
}
|
||||
|
||||
// Collection specifies the MongoDB collection for RoleDescription.
|
||||
func (*RoleDescription) Collection() string {
|
||||
return mservice.Roles
|
||||
}
|
||||
|
||||
// Role represents a role assignment for an account within an organization.
|
||||
type Role struct {
|
||||
AccountRef bson.ObjectID `bson:"accountRef" json:"accountRef"` // Account assigned to the role
|
||||
DescriptionRef bson.ObjectID `bson:"descriptionRef" json:"descriptionRef"` // Reference to the role's description
|
||||
OrganizationRef bson.ObjectID `bson:"organizationRef" json:"organizationRef"` // Organization where the role is applicable
|
||||
}
|
||||
|
||||
// ActionEffect represents a combination of an action and its effect (allow/deny).
|
||||
type ActionEffect struct {
|
||||
Action Action `bson:"action" json:"action"` // The action to perform (e.g., read, write)
|
||||
Effect Effect `bson:"effect" json:"effect"` // Whether the action is allowed or denied
|
||||
}
|
||||
|
||||
// Policy defines access control rules for a role within an organization.
|
||||
type Policy struct {
|
||||
OrganizationRef bson.ObjectID `bson:"organizationRef" json:"organizationRef"` // Organization associated with the policy
|
||||
DescriptionRef bson.ObjectID `bson:"descriptionRef" json:"descriptionRef"` // Reference to the policy's metadata
|
||||
ObjectRef *bson.ObjectID `bson:"objectRef,omitempty" json:"objectRef,omitempty"` // Target object (NilObjectID for all objects)
|
||||
Effect ActionEffect `bson:"effect" json:"effect"` // Action and effect for the policy
|
||||
}
|
||||
|
||||
// RolePolicy defines access control rules for a role within an organization.
|
||||
type RolePolicy struct {
|
||||
Policy `bson:",inline" json:",inline"`
|
||||
RoleDescriptionRef bson.ObjectID `bson:"roleDescriptionRef" json:"roleDescriptionRef"` // Reference to the associated role
|
||||
}
|
||||
|
||||
// PolicyDescription provides metadata for policies.
|
||||
type PolicyDescription struct {
|
||||
storable.Base `bson:",inline" json:",inline"` // Base fields for MongoDB documents
|
||||
Describable `bson:",inline" json:",inline"` // Name and description fields
|
||||
ResourceTypes *[]mservice.Type `bson:"resourceTypes,omitempty" json:"resourceTypes,omitempty"` // nil for custom policies, non-nil for built-in permissisons
|
||||
OrganizationRef *bson.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"` // nil for built-in policies, non-nil for custom
|
||||
}
|
||||
|
||||
// Collection specifies the MongoDB collection for PolicyDescription.
|
||||
func (*PolicyDescription) Collection() string {
|
||||
return mservice.Policies
|
||||
}
|
||||
|
||||
// Permission ties a policy to a specific account.
|
||||
type Permission struct {
|
||||
RolePolicy `bson:",inline" json:",inline"` // Embedded policy definition
|
||||
AccountRef bson.ObjectID `bson:"accountRef" json:"accountRef"` // Account assigned the permission
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type CardPaymentData struct {
|
||||
Pan string `bson:"pan" json:"pan"`
|
||||
FirstName string `bson:"firstName" json:"firstName"`
|
||||
LastName string `bson:"lastName" json:"lastName"`
|
||||
ExpMonth *int `bson:"exp_month" json:"exp_month"`
|
||||
ExpYear *int `bson:"exp_year" json:"exp_year"`
|
||||
Network string `bson:"network,omitempty" json:"network,omitempty"`
|
||||
Country string `bson:"country,omitempty" json:"country,omitempty"`
|
||||
}
|
||||
|
||||
func (d *CardPaymentData) UnmarshalBSON(data []byte) error {
|
||||
var raw map[string]any
|
||||
if err := bson.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Pan = paymentDataString(raw["pan"])
|
||||
d.FirstName = firstPaymentDataString(raw, "firstName", "first_name")
|
||||
d.LastName = firstPaymentDataString(raw, "lastName", "last_name")
|
||||
d.ExpMonth = firstPaymentDataInt(raw, "exp_month", "expMonth")
|
||||
d.ExpYear = firstPaymentDataInt(raw, "exp_year", "expYear")
|
||||
d.Network = paymentDataString(raw["network"])
|
||||
d.Country = paymentDataString(raw["country"])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PaymentMethod) AsCard() (*CardPaymentData, error) {
|
||||
if m.Type != PaymentTypeCard {
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not card", m.Type), "type")
|
||||
}
|
||||
var d CardPaymentData
|
||||
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func firstPaymentDataString(raw map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := paymentDataString(raw[key]); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// paymentDataDocument reads a nested payment data document as a map, whatever
|
||||
// document shape the BSON decoder produced: a generic decode yields bson.D,
|
||||
// while a typed or hand-built payload yields a map.
|
||||
func paymentDataDocument(value any) map[string]any {
|
||||
switch document := value.(type) {
|
||||
case map[string]any:
|
||||
return document
|
||||
case bson.M:
|
||||
return document
|
||||
case bson.D:
|
||||
out := make(map[string]any, len(document))
|
||||
for _, element := range document {
|
||||
out[element.Key] = element.Value
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func firstPaymentDataInt(raw map[string]any, keys ...string) *int {
|
||||
for _, key := range keys {
|
||||
if value := paymentDataInt(raw[key]); value != nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func paymentDataInt(value any) *int {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case int:
|
||||
return &v
|
||||
case int32:
|
||||
out := int(v)
|
||||
return &out
|
||||
case int64:
|
||||
out := int(v)
|
||||
return &out
|
||||
case uint:
|
||||
out := int(v)
|
||||
return &out
|
||||
case uint32:
|
||||
out := int(v)
|
||||
return &out
|
||||
case uint64:
|
||||
return paymentDataIntFromUint64(v)
|
||||
case float32:
|
||||
out := int(v)
|
||||
return &out
|
||||
case float64:
|
||||
out := int(v)
|
||||
return &out
|
||||
case string:
|
||||
clean := strings.TrimSpace(v)
|
||||
if clean == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := strconv.Atoi(clean)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &parsed
|
||||
default:
|
||||
clean := strings.TrimSpace(fmt.Sprint(v))
|
||||
if clean == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := strconv.Atoi(clean)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &parsed
|
||||
}
|
||||
}
|
||||
|
||||
func paymentDataIntFromUint64(value uint64) *int {
|
||||
if value > uint64(math.MaxInt) {
|
||||
return nil
|
||||
}
|
||||
out := int(value)
|
||||
return &out
|
||||
}
|
||||
|
||||
func paymentDataString(value any) string {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
case int:
|
||||
return fmt.Sprint(v)
|
||||
case int32:
|
||||
return fmt.Sprint(v)
|
||||
case int64:
|
||||
return fmt.Sprint(v)
|
||||
case uint:
|
||||
return fmt.Sprint(v)
|
||||
case uint32:
|
||||
return fmt.Sprint(v)
|
||||
case uint64:
|
||||
return fmt.Sprint(v)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(v), 'f', -1, 32)
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestPaymentMethodAsCardReadsStoredSnakeCaseExpiry(t *testing.T) {
|
||||
data, err := bson.Marshal(bson.M{
|
||||
"pan": "2200001100000001",
|
||||
"firstName": "S",
|
||||
"lastName": "S",
|
||||
"exp_month": 12.0,
|
||||
"exp_year": 2031.0,
|
||||
"country": nil,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal card data: %v", err)
|
||||
}
|
||||
|
||||
card, err := (&PaymentMethod{
|
||||
Type: PaymentTypeCard,
|
||||
Data: data,
|
||||
}).AsCard()
|
||||
if err != nil {
|
||||
t.Fatalf("AsCard returned error: %v", err)
|
||||
}
|
||||
|
||||
if got, want := card.ExpMonth, 12; got == nil || *got != want {
|
||||
t.Fatalf("exp_month mismatch: got=%v want=%d", got, want)
|
||||
}
|
||||
if got, want := card.ExpYear, 2031; got == nil || *got != want {
|
||||
t.Fatalf("exp_year mismatch: got=%v want=%d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardPaymentDataMarshalUsesStoredSnakeCaseExpiry(t *testing.T) {
|
||||
expMonth := 12
|
||||
expYear := 2031
|
||||
raw, err := bson.Marshal(CardPaymentData{
|
||||
Pan: "2200001100000001",
|
||||
FirstName: "S",
|
||||
LastName: "S",
|
||||
ExpMonth: &expMonth,
|
||||
ExpYear: &expYear,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal card data: %v", err)
|
||||
}
|
||||
|
||||
var data bson.M
|
||||
if err := bson.Unmarshal(raw, &data); err != nil {
|
||||
t.Fatalf("unmarshal card data: %v", err)
|
||||
}
|
||||
if got, want := paymentDataInt(data["exp_month"]), 12; got == nil || *got != want {
|
||||
t.Fatalf("stored exp_month mismatch: got=%v want=%d", got, want)
|
||||
}
|
||||
if got, want := paymentDataInt(data["exp_year"]), 2031; got == nil || *got != want {
|
||||
t.Fatalf("stored exp_year mismatch: got=%v want=%d", got, want)
|
||||
}
|
||||
if _, ok := data["expMonth"]; ok {
|
||||
t.Fatalf("card data must not store camelCase expMonth: %#v", data)
|
||||
}
|
||||
if _, ok := data["expYear"]; ok {
|
||||
t.Fatalf("card data must not store camelCase expYear: %#v", data)
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package model
|
||||
|
||||
import "strings"
|
||||
|
||||
// ChainNetwork identifies a blockchain network for routing and discovery.
|
||||
type ChainNetwork string
|
||||
|
||||
const (
|
||||
ChainNetworkArbitrumOne ChainNetwork = "arbitrum_one"
|
||||
ChainNetworkArbitrumSepolia ChainNetwork = "arbitrum_sepolia"
|
||||
ChainNetworkEthereumMainnet ChainNetwork = "ethereum_mainnet"
|
||||
ChainNetworkTronMainnet ChainNetwork = "tron_mainnet"
|
||||
ChainNetworkTronNile ChainNetwork = "tron_nile"
|
||||
ChainNetworkUnspecified ChainNetwork = ""
|
||||
)
|
||||
|
||||
// AllChainNetworks returns all valid chain networks.
|
||||
func AllChainNetworks() []ChainNetwork {
|
||||
return []ChainNetwork{
|
||||
ChainNetworkArbitrumOne,
|
||||
ChainNetworkArbitrumSepolia,
|
||||
ChainNetworkEthereumMainnet,
|
||||
ChainNetworkTronMainnet,
|
||||
ChainNetworkTronNile,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseChainNetwork parses a string into a ChainNetwork.
|
||||
// Returns the network and true if valid, or empty and false if invalid.
|
||||
func ParseChainNetwork(value string) (ChainNetwork, bool) {
|
||||
v := strings.ToLower(strings.TrimSpace(value))
|
||||
switch v {
|
||||
case "arbitrum_one", "arbitrum", "arb":
|
||||
return ChainNetworkArbitrumOne, true
|
||||
case "arbitrum_sepolia":
|
||||
return ChainNetworkArbitrumSepolia, true
|
||||
case "ethereum_mainnet", "ethereum", "eth":
|
||||
return ChainNetworkEthereumMainnet, true
|
||||
case "tron_mainnet", "tron":
|
||||
return ChainNetworkTronMainnet, true
|
||||
case "tron_nile", "nile":
|
||||
return ChainNetworkTronNile, true
|
||||
case "unspecified", "":
|
||||
return ChainNetworkUnspecified, true
|
||||
default:
|
||||
return ChainNetworkUnspecified, false
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the network.
|
||||
func (n ChainNetwork) String() string {
|
||||
return string(n)
|
||||
}
|
||||
|
||||
// IsValid returns true if the network is a known valid network.
|
||||
func (n ChainNetwork) IsValid() bool {
|
||||
switch n {
|
||||
case ChainNetworkArbitrumOne,
|
||||
ChainNetworkArbitrumSepolia,
|
||||
ChainNetworkEthereumMainnet,
|
||||
ChainNetworkTronMainnet,
|
||||
ChainNetworkTronNile:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsTron returns true if the network is a TRON network.
|
||||
func (n ChainNetwork) IsTron() bool {
|
||||
switch n {
|
||||
case ChainNetworkTronMainnet, ChainNetworkTronNile:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsEVM returns true if the network is an EVM-compatible network.
|
||||
func (n ChainNetwork) IsEVM() bool {
|
||||
switch n {
|
||||
case ChainNetworkArbitrumOne, ChainNetworkArbitrumSepolia, ChainNetworkEthereumMainnet:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsTestnet returns true if the network is a testnet.
|
||||
func (n ChainNetwork) IsTestnet() bool {
|
||||
switch n {
|
||||
case ChainNetworkTronNile, ChainNetworkArbitrumSepolia:
|
||||
return true
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestChainNetworkArbitrumSepoliaSupport(t *testing.T) {
|
||||
network, ok := ParseChainNetwork("arbitrum_sepolia")
|
||||
if !ok {
|
||||
t.Fatal("ParseChainNetwork(arbitrum_sepolia) returned ok=false")
|
||||
}
|
||||
if network != ChainNetworkArbitrumSepolia {
|
||||
t.Fatalf("ParseChainNetwork(arbitrum_sepolia) = %q, want %q", network, ChainNetworkArbitrumSepolia)
|
||||
}
|
||||
if !network.IsValid() {
|
||||
t.Fatal("ChainNetworkArbitrumSepolia should be valid")
|
||||
}
|
||||
if !network.IsEVM() {
|
||||
t.Fatal("ChainNetworkArbitrumSepolia should be EVM")
|
||||
}
|
||||
if !network.IsTestnet() {
|
||||
t.Fatal("ChainNetworkArbitrumSepolia should be testnet")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, candidate := range AllChainNetworks() {
|
||||
if candidate == ChainNetworkArbitrumSepolia {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("AllChainNetworks() is missing ChainNetworkArbitrumSepolia")
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
)
|
||||
|
||||
// ChainWalletRoute stores authoritative wallet-to-gateway routing metadata.
|
||||
type ChainWalletRoute struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
||||
WalletRef string `bson:"walletRef" json:"walletRef"`
|
||||
Network string `bson:"network" json:"network"`
|
||||
GatewayID string `bson:"gatewayId,omitempty" json:"gatewayId,omitempty"`
|
||||
}
|
||||
|
||||
func (*ChainWalletRoute) Collection() string {
|
||||
return mservice.WalletRoutes
|
||||
}
|
||||
|
||||
func (r *ChainWalletRoute) Normalize() {
|
||||
r.OrganizationRef = strings.TrimSpace(r.OrganizationRef)
|
||||
r.WalletRef = strings.TrimSpace(r.WalletRef)
|
||||
r.Network = strings.ToLower(strings.TrimSpace(r.Network))
|
||||
r.GatewayID = strings.TrimSpace(r.GatewayID)
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type CryptoAddressPaymentData struct {
|
||||
Currency Currency `bson:"currency" json:"currency"`
|
||||
Address string `bson:"address" json:"address"`
|
||||
Network string `bson:"network" json:"network"`
|
||||
DestinationTag *string `bson:"destinationTag,omitempty" json:"destinationTag,omitempty"`
|
||||
}
|
||||
|
||||
type cryptoAddressAssetJSON struct {
|
||||
Chain string `json:"chain,omitempty"`
|
||||
TokenSymbol string `json:"token_symbol,omitempty"`
|
||||
ContractAddress string `json:"contract_address,omitempty"`
|
||||
}
|
||||
|
||||
func (d CryptoAddressPaymentData) MarshalJSON() ([]byte, error) {
|
||||
var payload struct {
|
||||
Asset *cryptoAddressAssetJSON `json:"asset,omitempty"`
|
||||
Address string `json:"address"`
|
||||
Memo *string `json:"memo,omitempty"`
|
||||
}
|
||||
payload.Address = strings.TrimSpace(d.Address)
|
||||
if strings.TrimSpace(d.Network) != "" || strings.TrimSpace(string(d.Currency)) != "" {
|
||||
payload.Asset = &cryptoAddressAssetJSON{
|
||||
Chain: strings.TrimSpace(d.Network),
|
||||
TokenSymbol: strings.TrimSpace(string(d.Currency)),
|
||||
}
|
||||
}
|
||||
if memo := strings.TrimSpace(strPtr(d.DestinationTag)); memo != "" {
|
||||
payload.Memo = &memo
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
func (d *CryptoAddressPaymentData) UnmarshalJSON(data []byte) error {
|
||||
var raw struct {
|
||||
Asset *cryptoAddressAssetJSON `json:"asset"`
|
||||
Address string `json:"address"`
|
||||
Memo string `json:"memo"`
|
||||
Currency string `json:"currency"`
|
||||
Network string `json:"network"`
|
||||
DestinationTag *string `json:"destinationTag"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currency := strings.TrimSpace(raw.Currency)
|
||||
network := strings.TrimSpace(raw.Network)
|
||||
if raw.Asset != nil {
|
||||
if assetCurrency := strings.TrimSpace(raw.Asset.TokenSymbol); assetCurrency != "" {
|
||||
currency = assetCurrency
|
||||
}
|
||||
if assetNetwork := strings.TrimSpace(raw.Asset.Chain); assetNetwork != "" {
|
||||
network = assetNetwork
|
||||
}
|
||||
}
|
||||
|
||||
d.Currency = Currency(strings.ToUpper(currency))
|
||||
d.Address = strings.TrimSpace(raw.Address)
|
||||
d.Network = strings.ToUpper(network)
|
||||
d.DestinationTag = clonePaymentDataStringPtr(raw.Memo)
|
||||
if d.DestinationTag == nil && raw.DestinationTag != nil {
|
||||
d.DestinationTag = clonePaymentDataStringPtr(*raw.DestinationTag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CryptoAddressPaymentData) UnmarshalBSON(data []byte) error {
|
||||
var raw map[string]any
|
||||
if err := bson.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
currency := firstPaymentDataString(raw, "currency")
|
||||
network := firstPaymentDataString(raw, "network")
|
||||
if asset := paymentDataDocument(raw["asset"]); asset != nil {
|
||||
if assetCurrency := firstPaymentDataString(asset, "token_symbol", "tokenSymbol"); assetCurrency != "" {
|
||||
currency = assetCurrency
|
||||
}
|
||||
if assetNetwork := firstPaymentDataString(asset, "chain"); assetNetwork != "" {
|
||||
network = assetNetwork
|
||||
}
|
||||
}
|
||||
|
||||
d.Currency = Currency(strings.ToUpper(strings.TrimSpace(currency)))
|
||||
d.Address = firstPaymentDataString(raw, "address")
|
||||
d.Network = strings.ToUpper(strings.TrimSpace(network))
|
||||
d.DestinationTag = clonePaymentDataStringPtr(firstPaymentDataString(raw, "destinationTag", "destination_tag", "memo"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PaymentMethod) AsCryptoAddress() (*CryptoAddressPaymentData, error) {
|
||||
if m.Type != PaymentTypeCryptoAddress {
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not cryptoAddress", m.Type), "type")
|
||||
}
|
||||
|
||||
var d CryptoAddressPaymentData
|
||||
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func clonePaymentDataStringPtr(value string) *string {
|
||||
out := strings.TrimSpace(value)
|
||||
if out == "" {
|
||||
return nil
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
func strPtr(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestCryptoAddressPaymentDataReadsAssetFromGenericDocument(t *testing.T) {
|
||||
// The intermediate BSON document produced by PATCH decodes a nested asset
|
||||
// as bson.D before the payment service record is encoded.
|
||||
var payload any
|
||||
if err := json.Unmarshal([]byte(`{"asset":{"chain":"tron_mainnet","token_symbol":"usdt"},"address":"TXabc","memo":"tag-1"}`), &payload); err != nil {
|
||||
t.Fatalf("unmarshal payload: %v", err)
|
||||
}
|
||||
raw, err := bson.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal payload: %v", err)
|
||||
}
|
||||
|
||||
var data CryptoAddressPaymentData
|
||||
if err := bson.Unmarshal(raw, &data); err != nil {
|
||||
t.Fatalf("unmarshal crypto data: %v", err)
|
||||
}
|
||||
|
||||
if got, want := string(data.Currency), "USDT"; got != want {
|
||||
t.Fatalf("currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := data.Network, "TRON_MAINNET"; got != want {
|
||||
t.Fatalf("network mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := data.Address, "TXabc"; got != want {
|
||||
t.Fatalf("address mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := strPtr(data.DestinationTag), "tag-1"; got != want {
|
||||
t.Fatalf("destination tag mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCryptoAddressPaymentDataReadsAssetFromMapDocument(t *testing.T) {
|
||||
raw, err := bson.Marshal(bson.M{
|
||||
"asset": bson.M{"chain": "arbitrum_one", "tokenSymbol": "usdc"},
|
||||
"address": "0xabc",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal crypto data: %v", err)
|
||||
}
|
||||
|
||||
var data CryptoAddressPaymentData
|
||||
if err := bson.Unmarshal(raw, &data); err != nil {
|
||||
t.Fatalf("unmarshal crypto data: %v", err)
|
||||
}
|
||||
|
||||
if got, want := string(data.Currency), "USDC"; got != want {
|
||||
t.Fatalf("currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := data.Network, "ARBITRUM_ONE"; got != want {
|
||||
t.Fatalf("network mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCryptoAddressPaymentDataReadsStoredFlatShape(t *testing.T) {
|
||||
raw, err := bson.Marshal(CryptoAddressPaymentData{
|
||||
Currency: "USDT",
|
||||
Address: "TXabc",
|
||||
Network: "TRON_MAINNET",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal crypto data: %v", err)
|
||||
}
|
||||
|
||||
var data CryptoAddressPaymentData
|
||||
if err := bson.Unmarshal(raw, &data); err != nil {
|
||||
t.Fatalf("unmarshal crypto data: %v", err)
|
||||
}
|
||||
|
||||
if got, want := string(data.Currency), "USDT"; got != want {
|
||||
t.Fatalf("currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := data.Network, "TRON_MAINNET"; got != want {
|
||||
t.Fatalf("network mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// TokenPaymentData represents a network or gateway-issued card token.
|
||||
type TokenPaymentData struct {
|
||||
Token string `bson:"token" json:"token"`
|
||||
Network string `bson:"network,omitempty" json:"network,omitempty"`
|
||||
Last4 string `bson:"last4,omitempty" json:"masked_pan,omitempty"`
|
||||
ExpMonth *int `bson:"exp_month,omitempty" json:"exp_month,omitempty"`
|
||||
ExpYear *int `bson:"exp_year,omitempty" json:"exp_year,omitempty"`
|
||||
CardholderName string `bson:"cardholderName,omitempty" json:"cardholderName,omitempty"`
|
||||
Country string `bson:"country,omitempty" json:"country,omitempty"`
|
||||
}
|
||||
|
||||
func (d *TokenPaymentData) UnmarshalBSON(data []byte) error {
|
||||
var raw map[string]any
|
||||
if err := bson.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Token = paymentDataString(raw["token"])
|
||||
d.Network = paymentDataString(raw["network"])
|
||||
d.Last4 = firstPaymentDataString(raw, "last4", "last_4", "masked_pan")
|
||||
d.ExpMonth = firstPaymentDataInt(raw, "exp_month", "expMonth")
|
||||
d.ExpYear = firstPaymentDataInt(raw, "exp_year", "expYear")
|
||||
d.CardholderName = firstPaymentDataString(raw, "cardholderName", "cardholder_name")
|
||||
d.Country = paymentDataString(raw["country"])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PaymentMethod) AsCardToken() (*TokenPaymentData, error) {
|
||||
if m.Type != PaymentTypeCardToken {
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not cardToken", m.Type), "type")
|
||||
}
|
||||
|
||||
var d TokenPaymentData
|
||||
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestTokenPaymentDataUnmarshalReadsSnakeCaseExpiryAsInt(t *testing.T) {
|
||||
raw, err := bson.Marshal(bson.M{
|
||||
"token": "card-token",
|
||||
"last4": "1234",
|
||||
"exp_month": "01",
|
||||
"exp_year": 2031.0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal card token data: %v", err)
|
||||
}
|
||||
|
||||
var data TokenPaymentData
|
||||
if err := bson.Unmarshal(raw, &data); err != nil {
|
||||
t.Fatalf("unmarshal card token data: %v", err)
|
||||
}
|
||||
if got, want := data.ExpMonth, 1; got == nil || *got != want {
|
||||
t.Fatalf("exp_month mismatch: got=%v want=%d", got, want)
|
||||
}
|
||||
if got, want := data.ExpYear, 2031; got == nil || *got != want {
|
||||
t.Fatalf("exp_year mismatch: got=%v want=%d", got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/moneybase"
|
||||
)
|
||||
|
||||
type Currency string
|
||||
|
||||
const (
|
||||
CurrencyEUR Currency = "EUR" // Euro
|
||||
CurrencyUSD Currency = "USD" // US Dollar
|
||||
CurrencyRUB Currency = "RUB" // Russian Ruble
|
||||
CurrencyUAH Currency = "UAH" // Ukrainian Hryvnia
|
||||
CurrencyPLN Currency = "PLN" // Polish Złoty
|
||||
CurrencyCZK Currency = "CZK" // Czech Koruna
|
||||
CurrencyUSDT Currency = "USDT" // Tether USD
|
||||
)
|
||||
|
||||
const MaxCurrencyScale uint32 = moneybase.MaxScale
|
||||
|
||||
func NormalizeCurrency(value string) Currency {
|
||||
return Currency(strings.ToUpper(strings.TrimSpace(value)))
|
||||
}
|
||||
|
||||
func NormalizeExplicitCurrencyScales(values map[Currency]uint32) (map[Currency]uint32, error) {
|
||||
if len(values) == 0 {
|
||||
return nil, merrors.InvalidArgument("currency scales are required")
|
||||
}
|
||||
out := make(map[Currency]uint32, len(values))
|
||||
for rawCurrency, scale := range values {
|
||||
currency := NormalizeCurrency(string(rawCurrency))
|
||||
if currency == "" {
|
||||
return nil, merrors.InvalidArgument("currency scale key is required")
|
||||
}
|
||||
if scale > MaxCurrencyScale {
|
||||
return nil, merrors.InvalidArgumentf("currency scale for %s exceeds %d", currency, MaxCurrencyScale)
|
||||
}
|
||||
out[currency] = scale
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func CurrencyScale(scales map[Currency]uint32, currency string) (uint32, error) {
|
||||
normalized := NormalizeCurrency(currency)
|
||||
if normalized == "" {
|
||||
return 0, merrors.InvalidArgument("currency is required")
|
||||
}
|
||||
if scales == nil {
|
||||
return 0, merrors.InvalidArgument("currency scale map is required")
|
||||
}
|
||||
scale, ok := scales[normalized]
|
||||
if !ok {
|
||||
return 0, merrors.InvalidArgument("currency scale for " + string(normalized) + " is not configured")
|
||||
}
|
||||
return scale, nil
|
||||
}
|
||||
|
||||
func CurrencyScaleKeys(scales map[Currency]uint32) []string {
|
||||
if scales == nil {
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(scales))
|
||||
for currency := range scales {
|
||||
keys = append(keys, string(currency))
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
type Amount struct {
|
||||
Total float64 `bson:"total" json:"total"` // Total amount billed
|
||||
Currency Currency `bson:"currency" json:"currency"` // Currency for the invoice
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/moneybase"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMaxCurrencyScaleUsesSharedMoneyBaseRange(t *testing.T) {
|
||||
require.Equal(t, moneybase.MaxScale, MaxCurrencyScale)
|
||||
}
|
||||
|
||||
func TestNormalizeExplicitCurrencyScalesNormalizesKeys(t *testing.T) {
|
||||
scales, err := NormalizeExplicitCurrencyScales(map[Currency]uint32{"usd": 2, "usdt": 6})
|
||||
require.NoError(t, err)
|
||||
|
||||
usd, err := CurrencyScale(scales, "USD")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(2), usd)
|
||||
|
||||
usdt, err := CurrencyScale(scales, "USDT")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(6), usdt)
|
||||
}
|
||||
|
||||
func TestCurrencyScaleRejectsUnconfiguredCurrency(t *testing.T) {
|
||||
_, err := CurrencyScale(map[Currency]uint32{"USD": 2}, "KWD")
|
||||
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, merrors.ErrInvalidArg), "err=%v", err)
|
||||
}
|
||||
|
||||
func TestNormalizeExplicitCurrencyScalesRejectsMissingScales(t *testing.T) {
|
||||
_, err := NormalizeExplicitCurrencyScales(nil)
|
||||
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, merrors.ErrInvalidArg), "err=%v", err)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type IbanPaymentData struct {
|
||||
Iban string `bson:"iban" json:"iban"`
|
||||
AccountHolder string `bson:"accountHolder" json:"accountHolder"`
|
||||
Bic *string `bson:"bic,omitempty" json:"bic,omitempty"`
|
||||
BankName *string `bson:"bankName,omitempty" json:"bankName,omitempty"`
|
||||
}
|
||||
|
||||
func (m *PaymentMethod) AsIban() (*IbanPaymentData, error) {
|
||||
if m.Type != PaymentTypeIban {
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not iban", m.Type), "type")
|
||||
}
|
||||
var d IbanPaymentData
|
||||
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package model
|
||||
|
||||
import "git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
|
||||
type Indexable struct {
|
||||
Index int `bson:"index" json:"index"`
|
||||
}
|
||||
|
||||
type IndexableRef struct {
|
||||
storable.Ref `bson:",inline" json:",inline"`
|
||||
Indexable `bson:",inline" json:",inline"`
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type InvitationStatus string
|
||||
|
||||
const (
|
||||
InvitationCreated InvitationStatus = "created"
|
||||
InvitationSent InvitationStatus = "sent"
|
||||
InvitationAccepted InvitationStatus = "accepted"
|
||||
InvitationDeclined InvitationStatus = "declined"
|
||||
InvitationRevoked InvitationStatus = "revoked"
|
||||
)
|
||||
|
||||
type invitationDesc struct {
|
||||
Email string `bson:"email" json:"email"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
LastName string `bson:"lastName" json:"lastName"`
|
||||
Comment string `bson:"comment" json:"comment"`
|
||||
}
|
||||
|
||||
func (id *invitationDesc) Collection() string {
|
||||
return mservice.Invitations
|
||||
}
|
||||
|
||||
type Invitation struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
OrganizationRef bson.ObjectID `bson:"organizationRef" json:"organizationRef"`
|
||||
RoleRef bson.ObjectID `bson:"roleRef" json:"roleRef"`
|
||||
InviterRef bson.ObjectID `bson:"inviterRef" json:"inviterRef"`
|
||||
Status InvitationStatus `bson:"status" json:"status"`
|
||||
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
||||
Content invitationDesc `bson:"description" json:"description"`
|
||||
}
|
||||
|
||||
func (*Invitation) Collection() string {
|
||||
return mservice.Invitations
|
||||
}
|
||||
|
||||
type employeeDesc struct {
|
||||
Description Describable `bson:"description" json:"description"`
|
||||
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
|
||||
}
|
||||
|
||||
type organizationDesc struct {
|
||||
Description Describable `bson:"description" json:"description"`
|
||||
LogoURL *string `bson:"logoUrl,omitempty" json:"logoUrl,omitempty"`
|
||||
}
|
||||
|
||||
type invitationStorable struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
||||
}
|
||||
|
||||
type PublicInvitation struct {
|
||||
Storable invitationStorable `bson:"storable" json:"storable"`
|
||||
Employee employeeDesc `bson:"employee" json:"employee"`
|
||||
Organization organizationDesc `bson:"organization" json:"organization"`
|
||||
Role Describable `bson:"role" json:"role"`
|
||||
Invitation invitationDesc `bson:"invitation" json:"invitation"`
|
||||
RequiresRegistration bool `bson:"registrationRequired" json:"registrationRequired"`
|
||||
}
|
||||
|
||||
func (pi *PublicInvitation) Collection() string {
|
||||
return mservice.Invitations
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package model
|
||||
|
||||
func PendingInvitationStatuses() []InvitationStatus {
|
||||
return []InvitationStatus{InvitationCreated, InvitationSent}
|
||||
}
|
||||
|
||||
func PendingInvitationStatusValues() []string {
|
||||
statuses := PendingInvitationStatuses()
|
||||
values := make([]string, 0, len(statuses))
|
||||
for _, status := range statuses {
|
||||
values = append(values, string(status))
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func IsPendingInvitationStatus(status InvitationStatus) bool {
|
||||
return status == InvitationCreated || status == InvitationSent
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPendingInvitationStatusValues(t *testing.T) {
|
||||
got := PendingInvitationStatusValues()
|
||||
want := []string{"created", "sent"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("pending status count mismatch: got %d want %d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("pending status %d mismatch: got %q want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
|
||||
if !IsPendingInvitationStatus(InvitationCreated) {
|
||||
t.Fatal("created must be pending")
|
||||
}
|
||||
if !IsPendingInvitationStatus(InvitationSent) {
|
||||
t.Fatal("sent must be pending")
|
||||
}
|
||||
for _, status := range []InvitationStatus{InvitationAccepted, InvitationDeclined, InvitationRevoked} {
|
||||
if IsPendingInvitationStatus(status) {
|
||||
t.Fatalf("%q must not be pending", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvitationValidateEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid", email: "invitee@example.com"},
|
||||
{name: "whitespace padded valid", email: " invitee@example.com "},
|
||||
{name: "empty", email: "", wantErr: true},
|
||||
{name: "whitespace only", email: " ", wantErr: true},
|
||||
{name: "missing domain", email: "invitee@", wantErr: true},
|
||||
{name: "missing at", email: "invitee.example.com", wantErr: true},
|
||||
{name: "display name", email: "Invitee <invitee@example.com>", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inv := &Invitation{
|
||||
Status: InvitationCreated,
|
||||
Content: invitationDesc{
|
||||
Email: tt.email,
|
||||
},
|
||||
}
|
||||
err := inv.Validate()
|
||||
if tt.wantErr && err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Fatalf("expected valid invitation, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
)
|
||||
|
||||
func (i *Invitation) Validate() error {
|
||||
if i == nil {
|
||||
return merrors.InvalidArgument("invitation is required", "invitation")
|
||||
}
|
||||
if strings.TrimSpace(i.Content.Email) == "" {
|
||||
return merrors.InvalidArgument("invitation email must not be empty", "description.email")
|
||||
}
|
||||
trimmedEmail := strings.TrimSpace(i.Content.Email)
|
||||
parsed, err := mail.ParseAddress(trimmedEmail)
|
||||
if err != nil || parsed.Address != trimmedEmail {
|
||||
return merrors.InvalidArgument("invitation email is malformed", "description.email")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/model/account_role"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// AccountType defines the category of account (asset, liability, revenue, expense).
|
||||
type LedgerAccountType string
|
||||
|
||||
const (
|
||||
LedgerAccountTypeAsset LedgerAccountType = "asset"
|
||||
LedgerAccountTypeLiability LedgerAccountType = "liability"
|
||||
LedgerAccountTypeRevenue LedgerAccountType = "revenue"
|
||||
LedgerAccountTypeExpense LedgerAccountType = "expense"
|
||||
)
|
||||
|
||||
// AccountStatus tracks the operational state of an account.
|
||||
type LedgerAccountStatus string
|
||||
|
||||
const (
|
||||
LedgerAccountStatusActive LedgerAccountStatus = "active"
|
||||
LedgerAccountStatusFrozen LedgerAccountStatus = "frozen"
|
||||
LedgerAccountStatusClosed LedgerAccountStatus = "closed"
|
||||
)
|
||||
|
||||
type LedgerAccountScope string
|
||||
|
||||
const (
|
||||
LedgerAccountScopeOrganization LedgerAccountScope = "organization"
|
||||
LedgerAccountScopeSystem LedgerAccountScope = "system"
|
||||
)
|
||||
|
||||
type SystemAccountPurpose string
|
||||
|
||||
const (
|
||||
SystemAccountPurposeExternalSource SystemAccountPurpose = "external_source"
|
||||
SystemAccountPurposeExternalSink SystemAccountPurpose = "external_sink"
|
||||
)
|
||||
|
||||
// Account represents a ledger account that holds balances for a specific currency.
|
||||
type LedgerAccount struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
|
||||
// Scope defines whether the account belongs to an organization
|
||||
// or is a system-level ledger account used for internal accounting.
|
||||
Scope LedgerAccountScope `bson:"scope" json:"scope"`
|
||||
|
||||
// SystemPurpose specifies the role of a system-scoped account
|
||||
// (e.g., external source or sink of funds). Must be set for system accounts
|
||||
// and must be nil for organization accounts.
|
||||
SystemPurpose *SystemAccountPurpose `bson:"systemPurpose,omitempty" json:"systemPurpose,omitempty"`
|
||||
|
||||
// OrganizationRef links the account to an organization.
|
||||
// Must be set for organization accounts and nil for system accounts.
|
||||
OrganizationRef *bson.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"`
|
||||
|
||||
// Role defines the functional purpose of the account within an organization
|
||||
// (e.g., pending, operating, settlement, hold, etc.).
|
||||
// Must be set for organization accounts and omitted for system accounts.
|
||||
Role account_role.AccountRole `bson:"role,omitempty" json:"role,omitempty"`
|
||||
|
||||
// AccountCode is a logical classification code of the account
|
||||
// (e.g., "asset:cash:usd") used for reporting and grouping.
|
||||
AccountCode string `bson:"accountCode" json:"accountCode"`
|
||||
|
||||
// Currency is the ISO 4217 currency code the account operates in.
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
|
||||
// AccountType defines the accounting category of the account
|
||||
// (asset, liability, revenue, expense).
|
||||
AccountType LedgerAccountType `bson:"accountType" json:"accountType"`
|
||||
|
||||
// Status represents the operational state of the account.
|
||||
Status LedgerAccountStatus `bson:"status" json:"status"`
|
||||
|
||||
// AllowNegative defines whether the account is allowed to have
|
||||
// a negative balance (used for system control accounts).
|
||||
AllowNegative bool `bson:"allowNegative" json:"allowNegative"`
|
||||
|
||||
// OwnerRef optionally links the account to a specific owner entity
|
||||
// (e.g., user or sub-entity within the organization).
|
||||
OwnerRef *bson.ObjectID `bson:"ownerRef,omitempty" json:"ownerRef,omitempty"`
|
||||
|
||||
// Metadata holds additional arbitrary key-value attributes.
|
||||
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// Collection implements storable.Storable.
|
||||
func (*LedgerAccount) Collection() string {
|
||||
return mservice.LedgerAccounts
|
||||
}
|
||||
|
||||
// Validate enforces scope-specific invariants for ledger accounts.
|
||||
func (a *LedgerAccount) Validate() error {
|
||||
if a == nil {
|
||||
return merrors.InvalidArgument("ledger account is required")
|
||||
}
|
||||
|
||||
switch a.Scope {
|
||||
case LedgerAccountScopeOrganization:
|
||||
if a.OrganizationRef == nil || a.OrganizationRef.IsZero() {
|
||||
return merrors.InvalidArgument("organization_ref is required for organization accounts")
|
||||
}
|
||||
if strings.TrimSpace(string(a.Role)) == "" {
|
||||
return merrors.InvalidArgument("role is required for organization accounts")
|
||||
}
|
||||
if a.SystemPurpose != nil {
|
||||
return merrors.InvalidArgument("system_purpose must be nil for organization accounts")
|
||||
}
|
||||
case LedgerAccountScopeSystem:
|
||||
if a.OrganizationRef != nil && !a.OrganizationRef.IsZero() {
|
||||
return merrors.InvalidArgument("organization_ref must be nil for system accounts")
|
||||
}
|
||||
if strings.TrimSpace(string(a.Role)) != "" {
|
||||
return merrors.InvalidArgument("role must be empty for system accounts")
|
||||
}
|
||||
if a.SystemPurpose == nil {
|
||||
return merrors.InvalidArgument("system_purpose is required for system accounts")
|
||||
}
|
||||
default:
|
||||
return merrors.InvalidArgument("scope is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type TypeBound struct {
|
||||
Type mservice.Type `bson:"type" json:"type"`
|
||||
}
|
||||
|
||||
type ObjectRefs struct {
|
||||
TypeBound `bson:"inline" json:"inline"`
|
||||
Refs []bson.ObjectID `bson:"refs,omitempty" json:"refs,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectRef struct {
|
||||
TypeBound `bson:"inline" json:"inline"`
|
||||
Ref bson.ObjectID `bson:"ref,omitempty" json:"ref,omitempty"`
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type OrganizationSignupState string
|
||||
|
||||
const (
|
||||
OrganizationSignupStatePending OrganizationSignupState = "pending_signup_provisioning"
|
||||
OrganizationSignupStateBlocked OrganizationSignupState = "blocked_signup_provisioning"
|
||||
OrganizationSignupStateActive OrganizationSignupState = "active"
|
||||
)
|
||||
|
||||
type OrganizationBase struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
TenantRef bson.ObjectID `bson:"tenantRef" json:"tenantRef"`
|
||||
TimeZone string `bson:"timeZone" json:"timeZone"`
|
||||
LogoURL *string `bson:"logoUrl,omitempty" json:"logoUrl,omitempty"`
|
||||
SignupState OrganizationSignupState `bson:"signupState,omitempty" json:"signupState,omitempty"`
|
||||
}
|
||||
|
||||
func (*OrganizationBase) Collection() string {
|
||||
return mservice.Organizations
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
OrganizationBase `bson:",inline" json:",inline"`
|
||||
Members []bson.ObjectID `bson:"members" json:"members"`
|
||||
Provisioning *OrganizationProvisioning `bson:"provisioning,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
// OrganizationProvisioning tracks an organization that is being provisioned and
|
||||
// has no members or roles yet. It is the only link between such an organization
|
||||
// and its future owner, and it also guards concurrent provisioning attempts.
|
||||
// Activation clears only the lease fields: the provenance stays so a retried
|
||||
// create can recognize the organization it already activated.
|
||||
//
|
||||
// It lives on Organization rather than OrganizationBase on purpose: update
|
||||
// paths replace OrganizationBase wholesale from client payloads, which would
|
||||
// otherwise wipe it. It is never exposed over the API.
|
||||
type OrganizationProvisioning struct {
|
||||
OwnerRef bson.ObjectID `bson:"ownerRef" json:"-"`
|
||||
// Name snapshots the organization name of the first attempt. It is the
|
||||
// immutable half of the dedup key: renaming the organization must not
|
||||
// detach it from retries of the create that produced it.
|
||||
Name string `bson:"name,omitempty" json:"-"`
|
||||
Origin string `bson:"origin" json:"-"`
|
||||
ActorRef string `bson:"actorRef,omitempty" json:"-"`
|
||||
StartedAt time.Time `bson:"startedAt" json:"-"`
|
||||
LeaseOwner string `bson:"leaseOwner,omitempty" json:"-"`
|
||||
LeaseExpiresAt time.Time `bson:"leaseExpiresAt,omitempty" json:"-"`
|
||||
}
|
||||
|
||||
const (
|
||||
OrganizationSignupStateField = "signupState"
|
||||
OrganizationProvisioningField = "provisioning"
|
||||
OrganizationProvisioningOwnerRefField = "provisioning.ownerRef"
|
||||
OrganizationProvisioningNameField = "provisioning.name"
|
||||
OrganizationProvisioningLeaseOwnerField = "provisioning.leaseOwner"
|
||||
OrganizationProvisioningLeaseField = "provisioning.leaseExpiresAt"
|
||||
)
|
||||
|
||||
// LeaseHeld reports whether another attempt still owns the provisioning lease.
|
||||
func (p *OrganizationProvisioning) LeaseHeld(now time.Time) bool {
|
||||
return p != nil && p.LeaseExpiresAt.After(now)
|
||||
}
|
||||
|
||||
// CanFinishProvisioning reports whether an operator can drive this organization
|
||||
// to active through the backoffice create operation: only pending organizations
|
||||
// stamped by operator provisioning are resumable. Signup-era organizations
|
||||
// carry no provisioning stamp.
|
||||
func (o *Organization) CanFinishProvisioning() bool {
|
||||
return o != nil && o.Provisioning != nil && o.SignupState == OrganizationSignupStatePending
|
||||
}
|
||||
|
||||
func (o *Organization) NormalizeSignupState() {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
switch o.SignupState {
|
||||
case OrganizationSignupStatePending, OrganizationSignupStateBlocked, OrganizationSignupStateActive:
|
||||
default:
|
||||
o.SignupState = OrganizationSignupStateActive
|
||||
}
|
||||
}
|
||||
|
||||
type OrganizationBound interface {
|
||||
GetOrganizationRef() bson.ObjectID
|
||||
SetOrganizationRef(organizationRef bson.ObjectID)
|
||||
}
|
||||
|
||||
const (
|
||||
OrganizationRefField = "organizationRef"
|
||||
)
|
||||
|
||||
type OrganizationBoundBase struct {
|
||||
OrganizationRef bson.ObjectID `bson:"organizationRef" json:"organizationRef"`
|
||||
}
|
||||
|
||||
func (a *OrganizationBoundBase) GetOrganizationRef() bson.ObjectID {
|
||||
return a.OrganizationRef
|
||||
}
|
||||
|
||||
func (a *OrganizationBoundBase) SetOrganizationRef(organizationRef bson.ObjectID) {
|
||||
a.OrganizationRef = organizationRef
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// OwnerBound is implemented by resources that follow the personal-vs-org
|
||||
// ownership model (ownerKind + ownerRef). It is the type constraint for
|
||||
// ScopedDB and the canonical contract for scoped-access resources.
|
||||
//
|
||||
// permissionRef is deliberately absent: it is only meaningful for org-owned
|
||||
// resources and is supplied separately (as OrgPolicyRef) to the enforcement
|
||||
// layer. Mixing it here would re-introduce the very coupling this interface
|
||||
// is meant to eliminate.
|
||||
type OwnerBound interface {
|
||||
storable.Storable
|
||||
OrganizationBound
|
||||
GetOwnerKind() OwnerKind
|
||||
GetOwnerRef() bson.ObjectID
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package model
|
||||
|
||||
// OwnerKind describes who owns a scopable resource (ledger account, recipient,
|
||||
// payment method, …). It is the single source of truth for the personal-vs-org
|
||||
// access model: a resource is either owned by an individual account (personal)
|
||||
// or by an organization (org-scoped).
|
||||
//
|
||||
// The string values are persisted in Mongo and used in policy checks, so they
|
||||
// must stay stable. Scopable resources embed model.Owner (which carries an
|
||||
// OwnerKind) to keep one canonical vocabulary across the codebase.
|
||||
type OwnerKind string
|
||||
|
||||
const (
|
||||
// OwnerKindAccount marks a personal resource. Access is proven by ownership
|
||||
// alone: caller is allowed iff ownerRef == caller's account. No policy check.
|
||||
OwnerKindAccount OwnerKind = "account"
|
||||
// OwnerKindOrganization marks an org-scoped resource. Access is gated by the
|
||||
// relevant org policy via the Enforcer.
|
||||
OwnerKindOrganization OwnerKind = "organization"
|
||||
)
|
||||
|
||||
func (k OwnerKind) IsValid() bool {
|
||||
return k == OwnerKindAccount || k == OwnerKindOrganization
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type PaymentType int
|
||||
|
||||
const (
|
||||
PaymentTypeIban PaymentType = iota
|
||||
PaymentTypeCard
|
||||
PaymentTypeCardToken
|
||||
PaymentTypeBankAccount
|
||||
PaymentTypeWallet
|
||||
PaymentTypeCryptoAddress
|
||||
PaymentTypeLedger
|
||||
)
|
||||
|
||||
var paymentTypeToString = map[PaymentType]string{
|
||||
PaymentTypeIban: "iban",
|
||||
PaymentTypeCard: "card",
|
||||
PaymentTypeCardToken: "cardToken",
|
||||
PaymentTypeBankAccount: "bankAccount",
|
||||
PaymentTypeWallet: "wallet",
|
||||
PaymentTypeCryptoAddress: "cryptoAddress",
|
||||
PaymentTypeLedger: "ledger",
|
||||
}
|
||||
|
||||
var paymentTypeFromString = map[string]PaymentType{
|
||||
"iban": PaymentTypeIban,
|
||||
"card": PaymentTypeCard,
|
||||
"cardToken": PaymentTypeCardToken,
|
||||
"bankAccount": PaymentTypeBankAccount,
|
||||
"wallet": PaymentTypeWallet,
|
||||
"cryptoAddress": PaymentTypeCryptoAddress,
|
||||
"ledger": PaymentTypeLedger,
|
||||
}
|
||||
|
||||
func (t PaymentType) String() string {
|
||||
if v, ok := paymentTypeToString[t]; ok {
|
||||
return v
|
||||
}
|
||||
return "iban"
|
||||
}
|
||||
|
||||
func (t PaymentType) IsValid() bool {
|
||||
_, ok := paymentTypeToString[t]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (t PaymentType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.String())
|
||||
}
|
||||
|
||||
func (t *PaymentType) UnmarshalJSON(data []byte) error {
|
||||
var val string
|
||||
if err := json.Unmarshal(data, &val); err != nil {
|
||||
return err
|
||||
}
|
||||
v, ok := paymentTypeFromString[val]
|
||||
if !ok {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("unknown PaymentType: %q", val))
|
||||
}
|
||||
*t = v
|
||||
return nil
|
||||
}
|
||||
|
||||
type PaymentMethodOwnerKind string
|
||||
|
||||
const (
|
||||
PaymentMethodOwnerKindUnspecified PaymentMethodOwnerKind = ""
|
||||
PaymentMethodOwnerKindAccount PaymentMethodOwnerKind = "ACCOUNT"
|
||||
PaymentMethodOwnerKindRecipient PaymentMethodOwnerKind = "RECIPIENT"
|
||||
)
|
||||
|
||||
func (k PaymentMethodOwnerKind) String() string {
|
||||
return string(k)
|
||||
}
|
||||
|
||||
func (k PaymentMethodOwnerKind) IsValid() bool {
|
||||
switch k {
|
||||
case PaymentMethodOwnerKindAccount, PaymentMethodOwnerKindRecipient:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type PaymentMethod struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
|
||||
OwnerKind PaymentMethodOwnerKind `bson:"ownerKind" json:"ownerKind"`
|
||||
OwnerRef bson.ObjectID `bson:"ownerRef" json:"ownerRef"`
|
||||
Type PaymentType `bson:"type" json:"type"`
|
||||
Data bson.Raw `bson:"data" json:"data"`
|
||||
IsMain bool `bson:"isMain" json:"isMain"`
|
||||
}
|
||||
|
||||
type paymentMethodJSON struct {
|
||||
PermissionBound `json:",inline"`
|
||||
Describable `json:",inline"`
|
||||
|
||||
OwnerKind PaymentMethodOwnerKind `json:"ownerKind"`
|
||||
OwnerRef bson.ObjectID `json:"ownerRef"`
|
||||
Type PaymentType `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
IsMain bool `json:"isMain"`
|
||||
}
|
||||
|
||||
func (m PaymentMethod) MarshalJSON() ([]byte, error) {
|
||||
marshaledData, err := m.marshalDataJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload := paymentMethodJSON{
|
||||
PermissionBound: m.PermissionBound,
|
||||
Describable: m.Describable,
|
||||
OwnerKind: m.OwnerKind,
|
||||
OwnerRef: m.OwnerRef,
|
||||
Type: m.Type,
|
||||
Data: marshaledData,
|
||||
IsMain: m.IsMain,
|
||||
}
|
||||
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
func (m *PaymentMethod) UnmarshalJSON(data []byte) error {
|
||||
var payload paymentMethodJSON
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.PermissionBound = payload.PermissionBound
|
||||
m.Describable = payload.Describable
|
||||
m.OwnerKind = payload.OwnerKind
|
||||
m.OwnerRef = payload.OwnerRef
|
||||
m.Type = payload.Type
|
||||
m.IsMain = payload.IsMain
|
||||
|
||||
if len(payload.Data) == 0 || bytes.Equal(payload.Data, []byte("null")) {
|
||||
m.Data = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
if raw, ok, err := m.unmarshalTypedDataJSON(payload.Data); ok || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Data = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
var rawData map[string]any
|
||||
if err := json.Unmarshal(payload.Data, &rawData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
raw, err := bson.Marshal(rawData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Data = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m PaymentMethod) marshalDataJSON() (json.RawMessage, error) {
|
||||
if len(m.Data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var payload any
|
||||
var err error
|
||||
switch m.Type {
|
||||
case PaymentTypeIban:
|
||||
payload, err = marshalTypedPaymentData[IbanPaymentData](m.Data)
|
||||
case PaymentTypeCard:
|
||||
payload, err = marshalTypedPaymentData[CardPaymentData](m.Data)
|
||||
case PaymentTypeCardToken:
|
||||
payload, err = marshalTypedPaymentData[TokenPaymentData](m.Data)
|
||||
case PaymentTypeBankAccount:
|
||||
payload, err = marshalTypedPaymentData[RussianBankAccountPaymentData](m.Data)
|
||||
case PaymentTypeWallet:
|
||||
payload, err = marshalTypedPaymentData[WalletPaymentData](m.Data)
|
||||
case PaymentTypeCryptoAddress:
|
||||
payload, err = marshalTypedPaymentData[CryptoAddressPaymentData](m.Data)
|
||||
case PaymentTypeLedger:
|
||||
payload, err = marshalTypedPaymentData[LedgerPaymentData](m.Data)
|
||||
default:
|
||||
var data bson.M
|
||||
if err := bson.Unmarshal(m.Data, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
payload = data
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (m PaymentMethod) unmarshalTypedDataJSON(data json.RawMessage) (bson.Raw, bool, error) {
|
||||
switch m.Type {
|
||||
case PaymentTypeIban:
|
||||
return unmarshalTypedPaymentData[IbanPaymentData](data)
|
||||
case PaymentTypeCard:
|
||||
return unmarshalTypedPaymentData[CardPaymentData](data)
|
||||
case PaymentTypeCardToken:
|
||||
return unmarshalTypedPaymentData[TokenPaymentData](data)
|
||||
case PaymentTypeBankAccount:
|
||||
return unmarshalTypedPaymentData[RussianBankAccountPaymentData](data)
|
||||
case PaymentTypeWallet:
|
||||
return unmarshalTypedPaymentData[WalletPaymentData](data)
|
||||
case PaymentTypeCryptoAddress:
|
||||
return unmarshalTypedPaymentData[CryptoAddressPaymentData](data)
|
||||
case PaymentTypeLedger:
|
||||
return unmarshalTypedPaymentData[LedgerPaymentData](data)
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func marshalTypedPaymentData[T any](raw bson.Raw) (T, error) {
|
||||
var payload T
|
||||
err := bson.Unmarshal(raw, &payload)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
func unmarshalTypedPaymentData[T any](data json.RawMessage) (bson.Raw, bool, error) {
|
||||
var payload T
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
raw, err := bson.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return raw, true, nil
|
||||
}
|
||||
|
||||
type PaymentStatus string
|
||||
|
||||
const (
|
||||
PaymentStatusUnspecified PaymentStatus = "unspecified"
|
||||
|
||||
// Intent exists, no funds touched yet.
|
||||
PaymentStatusCreated PaymentStatus = "created"
|
||||
|
||||
// Internal Sendico operations: holds, ledger, FX, fee calc.
|
||||
PaymentStatusAuthorizing PaymentStatus = "authorizing"
|
||||
|
||||
// Funds are outside Sendico (rail/gateway/blockchain/provider).
|
||||
// Observe lives ONLY in this state.
|
||||
PaymentStatusExecuting PaymentStatus = "executing"
|
||||
|
||||
// Final states.
|
||||
PaymentStatusSucceeded PaymentStatus = "success"
|
||||
PaymentStatusFailed PaymentStatus = "failed"
|
||||
PaymentStatusCancelled PaymentStatus = "cancelled"
|
||||
)
|
||||
@@ -1,77 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type LedgerPaymentData struct {
|
||||
AccountRef string `bson:"accountRef,omitempty" json:"account_ref,omitempty"`
|
||||
AccountCode string `bson:"accountCode,omitempty" json:"account_code,omitempty"`
|
||||
ContraAccountRef string `bson:"contraAccountRef,omitempty" json:"contra_account_ref,omitempty"`
|
||||
ContraAccountCode string `bson:"contraAccountCode,omitempty" json:"contra_account_code,omitempty"`
|
||||
}
|
||||
|
||||
func (d *LedgerPaymentData) UnmarshalJSON(data []byte) error {
|
||||
var raw struct {
|
||||
AccountRef string `json:"account_ref"`
|
||||
LedgerAccountRef string `json:"ledger_account_ref"`
|
||||
AccountCode string `json:"account_code"`
|
||||
ContraAccountRef string `json:"contra_account_ref"`
|
||||
ContraLedgerAccountRef string `json:"contra_ledger_account_ref"`
|
||||
ContraAccountCode string `json:"contra_account_code"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accountRef := strings.TrimSpace(raw.AccountRef)
|
||||
ledgerAccountRef := strings.TrimSpace(raw.LedgerAccountRef)
|
||||
if ledgerAccountRef != "" {
|
||||
if accountRef != "" && accountRef != ledgerAccountRef {
|
||||
return merrors.InvalidArgument("account_ref and ledger_account_ref must match")
|
||||
}
|
||||
accountRef = ledgerAccountRef
|
||||
}
|
||||
|
||||
contraAccountRef := strings.TrimSpace(raw.ContraAccountRef)
|
||||
contraLedgerAccountRef := strings.TrimSpace(raw.ContraLedgerAccountRef)
|
||||
if contraLedgerAccountRef != "" {
|
||||
if contraAccountRef != "" && contraAccountRef != contraLedgerAccountRef {
|
||||
return merrors.InvalidArgument("contra_account_ref and contra_ledger_account_ref must match")
|
||||
}
|
||||
contraAccountRef = contraLedgerAccountRef
|
||||
}
|
||||
|
||||
d.AccountRef = accountRef
|
||||
d.AccountCode = strings.TrimSpace(raw.AccountCode)
|
||||
d.ContraAccountRef = contraAccountRef
|
||||
d.ContraAccountCode = strings.TrimSpace(raw.ContraAccountCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *LedgerPaymentData) UnmarshalBSON(data []byte) error {
|
||||
var raw map[string]any
|
||||
if err := bson.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
d.AccountRef = firstPaymentDataString(raw, "accountRef", "account_ref", "ledgerAccountRef", "ledger_account_ref")
|
||||
d.AccountCode = firstPaymentDataString(raw, "accountCode", "account_code")
|
||||
d.ContraAccountRef = firstPaymentDataString(raw, "contraAccountRef", "contra_account_ref", "contraLedgerAccountRef", "contra_ledger_account_ref")
|
||||
d.ContraAccountCode = firstPaymentDataString(raw, "contraAccountCode", "contra_account_code")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PaymentMethod) AsLedger() (*LedgerPaymentData, error) {
|
||||
if m.Type != PaymentTypeLedger {
|
||||
return nil, merrors.InvalidArgument("payment method type is "+m.Type.String()+", not ledger", "type")
|
||||
}
|
||||
var d LedgerPaymentData
|
||||
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/person"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestPaymentMethodMarshalRussianBankAccountUsesJSONRecipientNames(t *testing.T) {
|
||||
raw, err := bson.Marshal(RussianBankAccountPaymentData{
|
||||
Recipient: person.Name{
|
||||
FirstName: "Success",
|
||||
LastName: "Success",
|
||||
},
|
||||
Inn: "7707083893",
|
||||
Kpp: "770701001",
|
||||
BankName: "AsterBank Test Bank",
|
||||
Bik: "044525225",
|
||||
AccountNumber: "40702810000000080001",
|
||||
CorrespondentAccount: "30101810400000000225",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal bank account data: %v", err)
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(PaymentMethod{
|
||||
Type: PaymentTypeBankAccount,
|
||||
Data: raw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal payment method: %v", err)
|
||||
}
|
||||
|
||||
var decoded struct {
|
||||
Type PaymentType `json:"type"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal payload: %v", err)
|
||||
}
|
||||
if decoded.Type != PaymentTypeBankAccount {
|
||||
t.Fatalf("type mismatch: got=%s want=%s", decoded.Type, PaymentTypeBankAccount)
|
||||
}
|
||||
|
||||
recipient, ok := decoded.Data["recipient"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("recipient is not an object: %#v", decoded.Data["recipient"])
|
||||
}
|
||||
if got, want := recipient["first_name"], "Success"; got != want {
|
||||
t.Fatalf("recipient.first_name mismatch: got=%#v want=%q", got, want)
|
||||
}
|
||||
if got, want := recipient["last_name"], "Success"; got != want {
|
||||
t.Fatalf("recipient.last_name mismatch: got=%#v want=%q", got, want)
|
||||
}
|
||||
if _, ok := recipient["firstName"]; ok {
|
||||
t.Fatalf("recipient must not expose BSON firstName: %#v", recipient)
|
||||
}
|
||||
if _, ok := recipient["lastName"]; ok {
|
||||
t.Fatalf("recipient must not expose BSON lastName: %#v", recipient)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentMethodUnmarshalRussianBankAccountStoresBSONRecipientNames(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"type": "bankAccount",
|
||||
"data": {
|
||||
"recipient": {
|
||||
"first_name": "Success",
|
||||
"last_name": "Success"
|
||||
},
|
||||
"inn": "7707083893",
|
||||
"kpp": "770701001",
|
||||
"bankName": "AsterBank Test Bank",
|
||||
"bik": "044525225",
|
||||
"accountNumber": "40702810000000080001",
|
||||
"correspondentAccount": "30101810400000000225"
|
||||
}
|
||||
}`)
|
||||
|
||||
var method PaymentMethod
|
||||
if err := json.Unmarshal(payload, &method); err != nil {
|
||||
t.Fatalf("unmarshal payment method: %v", err)
|
||||
}
|
||||
|
||||
var stored bson.M
|
||||
if err := bson.Unmarshal(method.Data, &stored); err != nil {
|
||||
t.Fatalf("unmarshal stored data: %v", err)
|
||||
}
|
||||
recipient, ok := bsonDocumentMap(stored["recipient"])
|
||||
if !ok {
|
||||
t.Fatalf("stored recipient is not an object: %#v", stored["recipient"])
|
||||
}
|
||||
if got, want := recipient["firstName"], "Success"; got != want {
|
||||
t.Fatalf("stored recipient.firstName mismatch: got=%#v want=%q", got, want)
|
||||
}
|
||||
if got, want := recipient["lastName"], "Success"; got != want {
|
||||
t.Fatalf("stored recipient.lastName mismatch: got=%#v want=%q", got, want)
|
||||
}
|
||||
if _, ok := recipient["first_name"]; ok {
|
||||
t.Fatalf("stored recipient must not use JSON first_name: %#v", recipient)
|
||||
}
|
||||
if _, ok := recipient["last_name"]; ok {
|
||||
t.Fatalf("stored recipient must not use JSON last_name: %#v", recipient)
|
||||
}
|
||||
}
|
||||
|
||||
func bsonDocumentMap(value any) (map[string]any, bool) {
|
||||
switch typed := value.(type) {
|
||||
case bson.M:
|
||||
return typed, true
|
||||
case bson.D:
|
||||
out := make(map[string]any, len(typed))
|
||||
for _, element := range typed {
|
||||
out[element.Key] = element.Value
|
||||
}
|
||||
return out, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type PermissionBoundStorable interface {
|
||||
storable.Storable
|
||||
OrganizationBound
|
||||
Archivable
|
||||
GetPermissionRef() bson.ObjectID
|
||||
SetPermissionRef(permissionRef bson.ObjectID)
|
||||
}
|
||||
|
||||
type PermissionBound struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
ArchivableBase `bson:",inline" json:",inline"`
|
||||
OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||
PermissionRef bson.ObjectID `bson:"permissionRef" json:"permissionRef"`
|
||||
}
|
||||
|
||||
func (b *PermissionBound) GetPermissionRef() bson.ObjectID {
|
||||
return b.PermissionRef
|
||||
}
|
||||
|
||||
func (b *PermissionBound) GetOrganizationRef() bson.ObjectID {
|
||||
return b.OrganizationRef
|
||||
}
|
||||
|
||||
func (b *PermissionBound) SetPermissionRef(permissionRef bson.ObjectID) {
|
||||
b.PermissionRef = permissionRef
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/person"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type RussianBankAccountPaymentData struct {
|
||||
Recipient person.Name `bson:"recipient" json:"recipient"`
|
||||
Inn string `bson:"inn" json:"inn"`
|
||||
Kpp string `bson:"kpp" json:"kpp"`
|
||||
BankName string `bson:"bankName" json:"bankName"`
|
||||
Bik string `bson:"bik" json:"bik"`
|
||||
AccountNumber string `bson:"accountNumber" json:"accountNumber"`
|
||||
CorrespondentAccount string `bson:"correspondentAccount" json:"correspondentAccount"`
|
||||
}
|
||||
|
||||
// UnmarshalBSON accepts both stored recipient names and the API names in the
|
||||
// intermediate document built by a payment-method PATCH.
|
||||
func (d *RussianBankAccountPaymentData) UnmarshalBSON(data []byte) error {
|
||||
type stored RussianBankAccountPaymentData
|
||||
var decoded stored
|
||||
if err := bson.Unmarshal(data, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
var fields struct {
|
||||
Recipient map[string]any `bson:"recipient"`
|
||||
}
|
||||
if err := bson.Unmarshal(data, &fields); err != nil {
|
||||
return err
|
||||
}
|
||||
decoded.Recipient.FirstName = firstPaymentDataString(fields.Recipient, "firstName", "first_name")
|
||||
decoded.Recipient.LastName = firstPaymentDataString(fields.Recipient, "lastName", "last_name")
|
||||
*d = RussianBankAccountPaymentData(decoded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PaymentMethod) AsRussianBankAccount() (*RussianBankAccountPaymentData, error) {
|
||||
if m.Type != PaymentTypeBankAccount {
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not bankAccount", m.Type), "type")
|
||||
}
|
||||
|
||||
var d RussianBankAccountPaymentData
|
||||
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type RecipientStatus int
|
||||
|
||||
const (
|
||||
RecipientStatusReady RecipientStatus = iota
|
||||
RecipientStatusRegistered
|
||||
RecipientStatusNotRegistered
|
||||
)
|
||||
|
||||
var recipientStatusToString = map[RecipientStatus]string{
|
||||
RecipientStatusReady: "ready",
|
||||
RecipientStatusRegistered: "registered",
|
||||
RecipientStatusNotRegistered: "notRegistered",
|
||||
}
|
||||
|
||||
var recipientStatusFromString = map[string]RecipientStatus{
|
||||
"ready": RecipientStatusReady,
|
||||
"registered": RecipientStatusRegistered,
|
||||
"notRegistered": RecipientStatusNotRegistered,
|
||||
}
|
||||
|
||||
func (s RecipientStatus) String() string {
|
||||
if v, ok := recipientStatusToString[s]; ok {
|
||||
return v
|
||||
}
|
||||
return "ready" // дефолт, можно поменять
|
||||
}
|
||||
|
||||
// JSON: храним как строку ("ready" / "registered" / "notRegistered")
|
||||
func (s RecipientStatus) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
func (s *RecipientStatus) UnmarshalJSON(data []byte) error {
|
||||
var val string
|
||||
if err := json.Unmarshal(data, &val); err != nil {
|
||||
return err
|
||||
}
|
||||
v, ok := recipientStatusFromString[val]
|
||||
if !ok {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("unknown RecipientStatus: %q", val))
|
||||
}
|
||||
*s = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecipientType { internal, external }
|
||||
|
||||
type RecipientType int
|
||||
|
||||
const (
|
||||
RecipientTypeInternal RecipientType = iota
|
||||
RecipientTypeExternal
|
||||
)
|
||||
|
||||
var recipientTypeToString = map[RecipientType]string{
|
||||
RecipientTypeInternal: "internal",
|
||||
RecipientTypeExternal: "external",
|
||||
}
|
||||
|
||||
var recipientTypeFromString = map[string]RecipientType{
|
||||
"internal": RecipientTypeInternal,
|
||||
"external": RecipientTypeExternal,
|
||||
}
|
||||
|
||||
func (t RecipientType) String() string {
|
||||
if v, ok := recipientTypeToString[t]; ok {
|
||||
return v
|
||||
}
|
||||
return "internal"
|
||||
}
|
||||
|
||||
func (t RecipientType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.String())
|
||||
}
|
||||
|
||||
func (t *RecipientType) UnmarshalJSON(data []byte) error {
|
||||
var val string
|
||||
if err := json.Unmarshal(data, &val); err != nil {
|
||||
return err
|
||||
}
|
||||
v, ok := recipientTypeFromString[val]
|
||||
if !ok {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("unknown RecipientType: %q", val))
|
||||
}
|
||||
*t = v
|
||||
return nil
|
||||
}
|
||||
|
||||
// Owner is the ownership token stored under the "owner" BSON/JSON key.
|
||||
// storable.Ref must stay embedded: a named field would keep the bson ",inline"
|
||||
// but encoding/json has no inline option and would nest the ref under "Ref",
|
||||
// while embedding promotes it, so both wire paths are owner.ref and owner.kind.
|
||||
type Owner struct {
|
||||
storable.Ref `bson:",inline" json:",inline"`
|
||||
Kind OwnerKind `bson:"kind" json:"kind"`
|
||||
}
|
||||
|
||||
type Recipient struct {
|
||||
PermissionBound `bson:",inline" json:",inline"`
|
||||
Describable `bson:",inline" json:",inline"`
|
||||
Email string `bson:"email" json:"email"`
|
||||
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
|
||||
|
||||
Status RecipientStatus `bson:"status" json:"status"`
|
||||
Type RecipientType `bson:"type" json:"type"`
|
||||
Owner Owner `bson:"owner" json:"owner"`
|
||||
|
||||
// INN is the recipient's taxpayer identifier. It is the canonical source of
|
||||
// the INN for payouts to this recipient (flows into customer.tax_identifier),
|
||||
// replacing the per-payment-method inn field for recipient-owned methods.
|
||||
INN string `bson:"inn,omitempty" json:"inn,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Recipient) GetOwnerKind() OwnerKind { return r.Owner.Kind }
|
||||
func (r *Recipient) GetOwnerRef() bson.ObjectID { return r.Owner.Ref.Ref }
|
||||
func (r *Recipient) SetOwnerKind(k OwnerKind) { r.Owner.Kind = k }
|
||||
func (r *Recipient) SetOwnerRef(ref bson.ObjectID) { r.Owner.Ref.Ref = ref }
|
||||
@@ -1,62 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// TestOwnerJSONFlattensRef locks in the on-wire shape documented on Owner:
|
||||
// owner.ref and owner.kind must both be top-level keys. Ref must stay
|
||||
// anonymously embedded — a NAMED field with a `json:",inline"` tag is a
|
||||
// BSON-only convention and is not flattened by encoding/json, which would
|
||||
// silently nest it under the Go field name instead (a real regression this
|
||||
// test caught: {"Ref":{"ref":"..."},"kind":"..."} instead of
|
||||
// {"ref":"...","kind":"..."}), breaking every JSON consumer of owner.ref.
|
||||
func TestOwnerJSONFlattensRef(t *testing.T) {
|
||||
ownerRef := bson.NewObjectID()
|
||||
owner := Owner{Kind: OwnerKindAccount}
|
||||
owner.Ref.Ref = ownerRef
|
||||
|
||||
b, err := json.Marshal(owner)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal owner: %v", err)
|
||||
}
|
||||
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(b, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal owner json: %v", err)
|
||||
}
|
||||
if got, want := decoded["ref"], ownerRef.Hex(); got != want {
|
||||
t.Fatalf("top-level owner.ref = %v, want %q (got JSON: %s)", got, want, b)
|
||||
}
|
||||
if got, want := decoded["kind"], string(OwnerKindAccount); got != want {
|
||||
t.Fatalf("top-level owner.kind = %v, want %q (got JSON: %s)", got, want, b)
|
||||
}
|
||||
if _, nested := decoded["Ref"]; nested {
|
||||
t.Fatalf("owner.ref must not nest under the Go field name \"Ref\", got JSON: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipientJSONRoundTripsOwner(t *testing.T) {
|
||||
r := &Recipient{}
|
||||
r.SetOwnerKind(OwnerKindOrganization)
|
||||
r.SetOwnerRef(bson.NewObjectID())
|
||||
|
||||
b, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal recipient: %v", err)
|
||||
}
|
||||
|
||||
var roundTripped Recipient
|
||||
if err := json.Unmarshal(b, &roundTripped); err != nil {
|
||||
t.Fatalf("unmarshal recipient json: %v", err)
|
||||
}
|
||||
if roundTripped.GetOwnerKind() != r.GetOwnerKind() {
|
||||
t.Fatalf("owner kind round-trip = %v, want %v", roundTripped.GetOwnerKind(), r.GetOwnerKind())
|
||||
}
|
||||
if roundTripped.GetOwnerRef() != r.GetOwnerRef() {
|
||||
t.Fatalf("owner ref round-trip = %v, want %v", roundTripped.GetOwnerRef(), r.GetOwnerRef())
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// The frontend parses owner as {"ref": ..., "kind": ...}; a regression to the
|
||||
// nested {"Ref": {"ref": ...}} shape breaks every recipients screen.
|
||||
func TestOwnerJSONIsFlat(t *testing.T) {
|
||||
ref, err := bson.ObjectIDFromHex("69834f22fff13a7457793b6c")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := json.Marshal(Owner{
|
||||
Ref: storable.Ref{Ref: ref},
|
||||
Kind: "account",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := `{"ref":"69834f22fff13a7457793b6c","kind":"account"}`
|
||||
if string(out) != want {
|
||||
t.Fatalf("owner JSON = %s, want %s", out, want)
|
||||
}
|
||||
|
||||
var back Owner
|
||||
if err := json.Unmarshal(out, &back); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if back.Ref.Ref != ref || back.Kind != "account" {
|
||||
t.Fatalf("round-trip lost data: %+v", back)
|
||||
}
|
||||
}
|
||||
|
||||
// Mongo queries filter on owner.ref / owner.kind (see auth.ScopedDBImp), so
|
||||
// the BSON document must keep those exact paths.
|
||||
func TestOwnerBSONPathsUnchanged(t *testing.T) {
|
||||
ref, err := bson.ObjectIDFromHex("69834f22fff13a7457793b6c")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := bson.Marshal(Owner{
|
||||
Ref: storable.Ref{Ref: ref},
|
||||
Kind: "account",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var doc bson.M
|
||||
if err := bson.Unmarshal(out, &doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if doc["ref"] != ref || doc["kind"] != "account" {
|
||||
t.Fatalf("bson doc = %v, want flat ref/kind", doc)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package model
|
||||
|
||||
import "go.mongodb.org/mongo-driver/v2/bson"
|
||||
|
||||
// ScopedStorable is the type constraint for resources that follow the
|
||||
// personal-vs-org dual-ownership model. ScopedDBImp[T] provides generic
|
||||
// CRUD and bulk operations for any T satisfying this interface without
|
||||
// the permissionRef==NilObjectID sentinel pattern that ProtectedDBImp requires
|
||||
// for personal-owned resources.
|
||||
//
|
||||
// Resources that embed model.PermissionBound and model.Owner satisfy
|
||||
// ScopedStorable automatically once SetOwnerKind/SetOwnerRef methods are added.
|
||||
type ScopedStorable interface {
|
||||
OwnerBound
|
||||
SetOwnerKind(OwnerKind)
|
||||
SetOwnerRef(bson.ObjectID)
|
||||
GetPermissionRef() bson.ObjectID
|
||||
SetPermissionRef(bson.ObjectID)
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type SignupProvisioningState string
|
||||
|
||||
const (
|
||||
SignupProvisioningStateStarted SignupProvisioningState = "started"
|
||||
SignupProvisioningStateProvisioning SignupProvisioningState = "provisioning"
|
||||
SignupProvisioningStateCompensating SignupProvisioningState = "compensating"
|
||||
SignupProvisioningStateFailedPartialClean SignupProvisioningState = "failed_partial_cleanup"
|
||||
SignupProvisioningStateFailedClean SignupProvisioningState = "failed_clean"
|
||||
SignupProvisioningStateCompleted SignupProvisioningState = "completed"
|
||||
)
|
||||
|
||||
type SignupProvisioningCleanupStatus string
|
||||
|
||||
const (
|
||||
SignupProvisioningCleanupStatusNotNeeded SignupProvisioningCleanupStatus = "not_needed"
|
||||
SignupProvisioningCleanupStatusPending SignupProvisioningCleanupStatus = "pending"
|
||||
SignupProvisioningCleanupStatusRunning SignupProvisioningCleanupStatus = "running"
|
||||
SignupProvisioningCleanupStatusComplete SignupProvisioningCleanupStatus = "complete"
|
||||
SignupProvisioningCleanupStatusPartial SignupProvisioningCleanupStatus = "partial"
|
||||
)
|
||||
|
||||
type SignupProvisioningTarget struct {
|
||||
Chain ChainNetwork `bson:"chain" json:"chain"`
|
||||
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
||||
ContractAddress string `bson:"contractAddress,omitempty" json:"contractAddress,omitempty"`
|
||||
}
|
||||
|
||||
func (t *SignupProvisioningTarget) Normalize() {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
t.Chain = ChainNetwork(strings.TrimSpace(t.Chain.String()))
|
||||
t.TokenSymbol = strings.ToUpper(strings.TrimSpace(t.TokenSymbol))
|
||||
t.ContractAddress = strings.TrimSpace(t.ContractAddress)
|
||||
}
|
||||
|
||||
type SignupProvisionedWallet struct {
|
||||
Chain ChainNetwork `bson:"chain" json:"chain"`
|
||||
WalletRef string `bson:"walletRef" json:"walletRef"`
|
||||
}
|
||||
|
||||
func (w *SignupProvisionedWallet) Normalize() {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
w.Chain = ChainNetwork(strings.TrimSpace(w.Chain.String()))
|
||||
w.WalletRef = strings.TrimSpace(w.WalletRef)
|
||||
}
|
||||
|
||||
type SignupProvisionedRoute struct {
|
||||
WalletRef string `bson:"walletRef" json:"walletRef"`
|
||||
RouteRef string `bson:"routeRef" json:"routeRef"`
|
||||
}
|
||||
|
||||
func (r *SignupProvisionedRoute) Normalize() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.WalletRef = strings.TrimSpace(r.WalletRef)
|
||||
r.RouteRef = strings.TrimSpace(r.RouteRef)
|
||||
}
|
||||
|
||||
type SignupProvisioningRecord struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
NormalizedLogin string `bson:"normalizedLogin" json:"normalizedLogin"`
|
||||
OrganizationRef string `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"`
|
||||
AttemptID string `bson:"attemptId" json:"attemptId"`
|
||||
State SignupProvisioningState `bson:"state" json:"state"`
|
||||
CleanupStatus SignupProvisioningCleanupStatus `bson:"cleanupStatus" json:"cleanupStatus"`
|
||||
LeaseOwner string `bson:"leaseOwner" json:"leaseOwner"`
|
||||
LeaseExpiresAt time.Time `bson:"leaseExpiresAt" json:"leaseExpiresAt"`
|
||||
HeartbeatAt time.Time `bson:"heartbeatAt" json:"heartbeatAt"`
|
||||
SelectedTargets []SignupProvisioningTarget `bson:"selectedTargets,omitempty" json:"selectedTargets,omitempty"`
|
||||
Wallets []SignupProvisionedWallet `bson:"wallets,omitempty" json:"wallets,omitempty"`
|
||||
Routes []SignupProvisionedRoute `bson:"routes,omitempty" json:"routes,omitempty"`
|
||||
StepStatus map[string]string `bson:"stepStatus,omitempty" json:"stepStatus,omitempty"`
|
||||
LastError string `bson:"lastError,omitempty" json:"lastError,omitempty"`
|
||||
LastCleanupError string `bson:"lastCleanupError,omitempty" json:"lastCleanupError,omitempty"`
|
||||
ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
func (*SignupProvisioningRecord) Collection() string {
|
||||
return mservice.SignupProvisioning
|
||||
}
|
||||
|
||||
func (r *SignupProvisioningRecord) Normalize() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.NormalizedLogin = strings.ToLower(strings.TrimSpace(r.NormalizedLogin))
|
||||
r.OrganizationRef = strings.TrimSpace(r.OrganizationRef)
|
||||
r.AttemptID = strings.TrimSpace(r.AttemptID)
|
||||
r.LeaseOwner = strings.TrimSpace(r.LeaseOwner)
|
||||
r.LastError = strings.TrimSpace(r.LastError)
|
||||
r.LastCleanupError = strings.TrimSpace(r.LastCleanupError)
|
||||
|
||||
for i := range r.SelectedTargets {
|
||||
r.SelectedTargets[i].Normalize()
|
||||
}
|
||||
for i := range r.Wallets {
|
||||
r.Wallets[i].Normalize()
|
||||
}
|
||||
for i := range r.Routes {
|
||||
r.Routes[i].Normalize()
|
||||
}
|
||||
if r.StepStatus == nil {
|
||||
r.StepStatus = map[string]string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SignupProvisioningRecord) Active() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
switch r.State {
|
||||
case SignupProvisioningStateStarted, SignupProvisioningStateProvisioning, SignupProvisioningStateCompensating:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SignupProvisioningRecord) Terminal() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
switch r.State {
|
||||
case SignupProvisioningStateFailedPartialClean, SignupProvisioningStateFailedClean, SignupProvisioningStateCompleted:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SignupProvisioningRecord) OrganizationObjectID() bson.ObjectID {
|
||||
ref, _ := bson.ObjectIDFromHex(strings.TrimSpace(r.OrganizationRef))
|
||||
return ref
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type WalletPaymentData struct {
|
||||
WalletID string `bson:"walletId" json:"walletId"`
|
||||
}
|
||||
|
||||
func (m *PaymentMethod) AsWallet() (*WalletPaymentData, error) {
|
||||
if m.Type != PaymentTypeWallet {
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not wallet", m.Type), "type")
|
||||
}
|
||||
|
||||
var d WalletPaymentData
|
||||
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/db/storable"
|
||||
"git.sendico.io/tech/sxchnge/bff/pkg/mservice"
|
||||
)
|
||||
|
||||
type WalletProvisioningState string
|
||||
|
||||
const (
|
||||
WalletProvisioningStateProvisioning WalletProvisioningState = "provisioning"
|
||||
WalletProvisioningStateCompleted WalletProvisioningState = "completed"
|
||||
WalletProvisioningStateFailed WalletProvisioningState = "failed"
|
||||
)
|
||||
|
||||
type WalletProvisioningTargetState string
|
||||
|
||||
const (
|
||||
WalletProvisioningTargetStatePending WalletProvisioningTargetState = "pending"
|
||||
WalletProvisioningTargetStateWalletReady WalletProvisioningTargetState = "wallet_ready"
|
||||
WalletProvisioningTargetStateSuspended WalletProvisioningTargetState = "suspended"
|
||||
WalletProvisioningTargetStateRouted WalletProvisioningTargetState = "routed"
|
||||
)
|
||||
|
||||
type WalletProvisioningTarget struct {
|
||||
TargetID string `bson:"targetId" json:"targetId"`
|
||||
Chain ChainNetwork `bson:"chain" json:"chain"`
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
ContractAddress string `bson:"contractAddress,omitempty" json:"contractAddress,omitempty"`
|
||||
GatewayID string `bson:"gatewayId,omitempty" json:"gatewayId,omitempty"`
|
||||
GatewayURI string `bson:"gatewayUri,omitempty" json:"gatewayUri,omitempty"`
|
||||
WalletIdempotencyKey string `bson:"walletIdempotencyKey,omitempty" json:"walletIdempotencyKey,omitempty"`
|
||||
State WalletProvisioningTargetState `bson:"state" json:"state"`
|
||||
WalletRef string `bson:"walletRef,omitempty" json:"walletRef,omitempty"`
|
||||
Address string `bson:"address,omitempty" json:"address,omitempty"`
|
||||
RouteRef string `bson:"routeRef,omitempty" json:"routeRef,omitempty"`
|
||||
LastError string `bson:"lastError,omitempty" json:"lastError,omitempty"`
|
||||
}
|
||||
|
||||
func (t *WalletProvisioningTarget) Normalize() {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
t.TargetID = strings.TrimSpace(t.TargetID)
|
||||
t.Currency = strings.ToUpper(strings.TrimSpace(t.Currency))
|
||||
t.ContractAddress = strings.TrimSpace(t.ContractAddress)
|
||||
if strings.HasPrefix(strings.ToLower(t.ContractAddress), "0x") {
|
||||
t.ContractAddress = strings.ToLower(t.ContractAddress)
|
||||
}
|
||||
t.GatewayID = strings.TrimSpace(t.GatewayID)
|
||||
t.GatewayURI = strings.TrimSpace(t.GatewayURI)
|
||||
t.WalletIdempotencyKey = strings.TrimSpace(t.WalletIdempotencyKey)
|
||||
t.WalletRef = strings.TrimSpace(t.WalletRef)
|
||||
t.Address = strings.TrimSpace(t.Address)
|
||||
t.RouteRef = strings.TrimSpace(t.RouteRef)
|
||||
t.LastError = strings.TrimSpace(t.LastError)
|
||||
if t.State == "" {
|
||||
t.State = WalletProvisioningTargetStatePending
|
||||
}
|
||||
}
|
||||
|
||||
type WalletProvisioningRecord struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
ProvisioningRef string `bson:"provisioningRef" json:"provisioningRef"`
|
||||
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
||||
OwnerRef string `bson:"ownerRef,omitempty" json:"ownerRef,omitempty"`
|
||||
LedgerAccountRef string `bson:"ledgerAccountRef" json:"ledgerAccountRef"`
|
||||
ActorRef string `bson:"actorRef,omitempty" json:"actorRef,omitempty"`
|
||||
Reason string `bson:"reason" json:"reason"`
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
State WalletProvisioningState `bson:"state" json:"state"`
|
||||
Targets []WalletProvisioningTarget `bson:"targets" json:"targets"`
|
||||
LastError string `bson:"lastError,omitempty" json:"lastError,omitempty"`
|
||||
// Version fences concurrent journal writers: Save replaces only the
|
||||
// revision this attempt read, so a stale attempt whose lease was taken
|
||||
// over cannot roll back the checkpoints of the attempt that owns it now.
|
||||
Version int64 `bson:"version" json:"-"`
|
||||
}
|
||||
|
||||
func (*WalletProvisioningRecord) Collection() string {
|
||||
return mservice.WalletProvisioning
|
||||
}
|
||||
|
||||
func (r *WalletProvisioningRecord) Normalize() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.ProvisioningRef = strings.TrimSpace(r.ProvisioningRef)
|
||||
r.OrganizationRef = strings.TrimSpace(r.OrganizationRef)
|
||||
r.OwnerRef = strings.TrimSpace(r.OwnerRef)
|
||||
r.LedgerAccountRef = strings.TrimSpace(r.LedgerAccountRef)
|
||||
r.ActorRef = strings.TrimSpace(r.ActorRef)
|
||||
r.Reason = strings.TrimSpace(r.Reason)
|
||||
r.Currency = strings.ToUpper(strings.TrimSpace(r.Currency))
|
||||
r.LastError = strings.TrimSpace(r.LastError)
|
||||
for index := range r.Targets {
|
||||
r.Targets[index].Normalize()
|
||||
}
|
||||
if r.State == "" {
|
||||
r.State = WalletProvisioningStateProvisioning
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user