From 5e1da9617fb8883bc9fe5152ce5eb62fd6e97611 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Mon, 1 Dec 2025 21:20:10 +0100 Subject: [PATCH] +address book service --- api/pkg/db/factory.go | 4 + api/pkg/db/internal/mongo/db.go | 27 +++++ .../db/internal/mongo/paymethoddb/archived.go | 20 ++++ api/pkg/db/internal/mongo/paymethoddb/db.go | 49 ++++++++ api/pkg/db/internal/mongo/paymethoddb/list.go | 28 +++++ .../db/internal/mongo/recipientdb/archived.go | 57 ++++++++++ api/pkg/db/internal/mongo/recipientdb/db.go | 56 +++++++++ api/pkg/db/internal/mongo/recipientdb/list.go | 28 +++++ api/pkg/db/invitation/invitation.go | 2 +- api/pkg/db/paymethod/db.go | 15 +++ api/pkg/db/recipient/db.go | 15 +++ api/pkg/model/card.go | 25 +++++ api/pkg/model/iban.go | 26 +++++ api/pkg/model/payment.go | 66 +++++++++++ api/pkg/model/rba.go | 31 +++++ api/pkg/model/recipient.go | 106 ++++++++++++++++++ api/pkg/model/wallet.go | 25 +++++ api/pkg/mservice/services.go | 2 + .../interface/services/paymethod/paymethod.go | 11 ++ .../interface/services/recipient/recipient.go | 11 ++ api/server/internal/api/api.go | 6 +- .../internal/server/paymethodsimp/service.go | 46 ++++++++ .../internal/server/recipientimp/service.go | 46 ++++++++ frontend/pshared/lib/service/services.dart | 3 + 24 files changed, 703 insertions(+), 2 deletions(-) create mode 100644 api/pkg/db/internal/mongo/paymethoddb/archived.go create mode 100644 api/pkg/db/internal/mongo/paymethoddb/db.go create mode 100644 api/pkg/db/internal/mongo/paymethoddb/list.go create mode 100644 api/pkg/db/internal/mongo/recipientdb/archived.go create mode 100644 api/pkg/db/internal/mongo/recipientdb/db.go create mode 100644 api/pkg/db/internal/mongo/recipientdb/list.go create mode 100644 api/pkg/db/paymethod/db.go create mode 100644 api/pkg/db/recipient/db.go create mode 100644 api/pkg/model/card.go create mode 100644 api/pkg/model/iban.go create mode 100644 api/pkg/model/payment.go create mode 100644 api/pkg/model/rba.go create mode 100644 api/pkg/model/recipient.go create mode 100644 api/pkg/model/wallet.go create mode 100644 api/server/interface/services/paymethod/paymethod.go create mode 100644 api/server/interface/services/recipient/recipient.go create mode 100644 api/server/internal/server/paymethodsimp/service.go create mode 100644 api/server/internal/server/recipientimp/service.go diff --git a/api/pkg/db/factory.go b/api/pkg/db/factory.go index 3c37aac..cc4dfa8 100644 --- a/api/pkg/db/factory.go +++ b/api/pkg/db/factory.go @@ -7,7 +7,9 @@ import ( mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo" "github.com/tech/sendico/pkg/db/invitation" "github.com/tech/sendico/pkg/db/organization" + "github.com/tech/sendico/pkg/db/paymethod" "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/recipient" "github.com/tech/sendico/pkg/db/refreshtokens" "github.com/tech/sendico/pkg/db/role" "github.com/tech/sendico/pkg/db/transaction" @@ -23,6 +25,8 @@ type Factory interface { NewAccountDB() (account.DB, error) NewOrganizationDB() (organization.DB, error) NewInvitationsDB() (invitation.DB, error) + NewRecipientsDB() (recipient.DB, error) + NewPaymentMethodsDB() (paymethod.DB, error) NewRolesDB() (role.DB, error) NewPoliciesDB() (policy.DB, error) diff --git a/api/pkg/db/internal/mongo/db.go b/api/pkg/db/internal/mongo/db.go index e73eb39..3e4e2f6 100755 --- a/api/pkg/db/internal/mongo/db.go +++ b/api/pkg/db/internal/mongo/db.go @@ -15,13 +15,17 @@ import ( "github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb" "github.com/tech/sendico/pkg/db/internal/mongo/invitationdb" "github.com/tech/sendico/pkg/db/internal/mongo/organizationdb" + "github.com/tech/sendico/pkg/db/internal/mongo/paymethoddb" "github.com/tech/sendico/pkg/db/internal/mongo/policiesdb" + "github.com/tech/sendico/pkg/db/internal/mongo/recipientdb" "github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb" "github.com/tech/sendico/pkg/db/internal/mongo/rolesdb" "github.com/tech/sendico/pkg/db/internal/mongo/transactionimp" "github.com/tech/sendico/pkg/db/invitation" "github.com/tech/sendico/pkg/db/organization" + "github.com/tech/sendico/pkg/db/paymethod" "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/recipient" "github.com/tech/sendico/pkg/db/refreshtokens" "github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/role" @@ -201,6 +205,29 @@ func (db *DB) NewOrganizationDB() (organization.DB, error) { return organizationDB, nil } +func (db *DB) NewRecipientsDB() (recipient.DB, error) { + pmdb, err := db.NewPaymentMethodsDB() + if err != nil { + db.logger.Warn("Failed to create payment methods database", zap.Error(err)) + return nil, err + } + + create := func(ctx context.Context, + logger mlogger.Logger, + enforcer auth.Enforcer, + pdb policy.DB, + db *mongo.Database, + ) (recipient.DB, error) { + return recipientdb.Create(ctx, logger, enforcer, pdb, pmdb, db) + } + + return newProtectedDB(db, create) +} + +func (db *DB) NewPaymentMethodsDB() (paymethod.DB, error) { + return newProtectedDB(db, paymethoddb.Create) +} + func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) { return refreshtokensdb.Create(db.logger, db.db()) } diff --git a/api/pkg/db/internal/mongo/paymethoddb/archived.go b/api/pkg/db/internal/mongo/paymethoddb/archived.go new file mode 100644 index 0000000..13c8e35 --- /dev/null +++ b/api/pkg/db/internal/mongo/paymethoddb/archived.go @@ -0,0 +1,20 @@ +package paymethoddb + +import ( + "context" + + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func (db *PaymentMethodsDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef primitive.ObjectID, isArchived, cascade bool) error { + // Use the ArchivableDB for the main archiving logic + if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil { + db.DBImp.Logger.Warn("Failed to chnage object archive status", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), + mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade)) + return err + } + return nil +} diff --git a/api/pkg/db/internal/mongo/paymethoddb/db.go b/api/pkg/db/internal/mongo/paymethoddb/db.go new file mode 100644 index 0000000..40ce16c --- /dev/null +++ b/api/pkg/db/internal/mongo/paymethoddb/db.go @@ -0,0 +1,49 @@ +package paymethoddb + +import ( + "context" + + "github.com/tech/sendico/pkg/auth" + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/mongo" +) + +type PaymentMethodsDB struct { + auth.ProtectedDBImp[*model.PaymentMethod] + auth.ArchivableDB[*model.PaymentMethod] +} + +func Create(ctx context.Context, + logger mlogger.Logger, + enforcer auth.Enforcer, + pdb policy.DB, + db *mongo.Database, +) (*PaymentMethodsDB, error) { + p, err := auth.CreateDBImp[*model.PaymentMethod](ctx, logger, pdb, enforcer, mservice.PaymentMethods, db) + if err != nil { + return nil, err + } + + createEmpty := func() *model.PaymentMethod { + return &model.PaymentMethod{} + } + + getArchivable := func(c *model.PaymentMethod) model.Archivable { + return &c.ArchivableBase + } + + res := &PaymentMethodsDB{ + ProtectedDBImp: *p, + ArchivableDB: auth.NewArchivableDB( + p.DBImp, + logger, + p.Enforcer, + createEmpty, + getArchivable, + ), + } + return res, nil +} diff --git a/api/pkg/db/internal/mongo/paymethoddb/list.go b/api/pkg/db/internal/mongo/paymethoddb/list.go new file mode 100644 index 0000000..772c3b5 --- /dev/null +++ b/api/pkg/db/internal/mongo/paymethoddb/list.go @@ -0,0 +1,28 @@ +package paymethoddb + +import ( + "context" + "errors" + + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" + mauth "github.com/tech/sendico/pkg/mutil/db/auth" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func (db *PaymentMethodsDB) List(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, cursor *model.ViewCursor) ([]model.PaymentMethod, error) { + res, err := mauth.GetProtectedObjects[model.PaymentMethod]( + ctx, + db.DBImp.Logger, + accountRef, organizationRef, model.ActionRead, + repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)), + cursor, + db.Enforcer, + db.DBImp.Repository, + ) + if errors.Is(err, merrors.ErrNoData) { + return []model.PaymentMethod{}, nil + } + return res, err +} diff --git a/api/pkg/db/internal/mongo/recipientdb/archived.go b/api/pkg/db/internal/mongo/recipientdb/archived.go new file mode 100644 index 0000000..c0af4dc --- /dev/null +++ b/api/pkg/db/internal/mongo/recipientdb/archived.go @@ -0,0 +1,57 @@ +package recipientdb + +import ( + "context" + "errors" + + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func (db *RecipientDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef primitive.ObjectID, isArchived, cascade bool) error { + // Use the ArchivableDB for the main archiving logic + if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil { + db.DBImp.Logger.Warn("Failed to change recipient archive status", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), + mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade)) + return err + } + + if cascade { + if err := db.setArchivedPaymentMethods(ctx, accountRef, organizationRef, objectRef, isArchived); err != nil { + db.DBImp.Logger.Warn("Failed to update payment methods archive status", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), + mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade)) + + return err + } + } + + return nil +} + +func (db *RecipientDB) setArchivedPaymentMethods(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, archived bool) error { + db.DBImp.Logger.Debug("Setting archived status for recipient payment methods", mzap.ObjRef("recipient_ref", recipientRef), zap.Bool("archived", archived)) + + db.DBImp.Logger.Debug("Applying archived status to payment methods for recipient", mzap.ObjRef("recipient_ref", recipientRef)) + + // Get all payMethods for the recipient + payMethods, err := db.pmdb.List(ctx, accountRef, organizationRef, recipientRef, nil) + if err != nil && !errors.Is(err, merrors.ErrNoData) { + db.DBImp.Logger.Warn("Failed to fetch payment methods for recipient", zap.Error(err), mzap.ObjRef("recipient_ref", recipientRef)) + return err + } + + // Archive each payment method + for _, pmethod := range payMethods { + if err := db.pmdb.SetArchived(ctx, accountRef, organizationRef, pmethod.ID, archived, true); err != nil { + db.DBImp.Logger.Warn("Failed to set archived status for payment method", zap.Error(err), mzap.ObjRef("payment_method_ref", pmethod.ID)) + return err + } + } + + db.DBImp.Logger.Debug("Successfully updated payment methods archived status", zap.Int("count", len(payMethods)), mzap.ObjRef("recipient_ref", recipientRef)) + return nil +} diff --git a/api/pkg/db/internal/mongo/recipientdb/db.go b/api/pkg/db/internal/mongo/recipientdb/db.go new file mode 100644 index 0000000..8fd4ef3 --- /dev/null +++ b/api/pkg/db/internal/mongo/recipientdb/db.go @@ -0,0 +1,56 @@ +package recipientdb + +import ( + "context" + + "github.com/tech/sendico/pkg/auth" + "github.com/tech/sendico/pkg/db/paymethod" + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/mongo" +) + +type RecipientDB struct { + auth.ProtectedDBImp[*model.Recipient] + auth.ArchivableDB[*model.Recipient] + pmdb paymethod.DB + paymentMethodsRepo repository.Repository +} + +func Create(ctx context.Context, + logger mlogger.Logger, + enforcer auth.Enforcer, + pdb policy.DB, + pmdb paymethod.DB, + db *mongo.Database, +) (*RecipientDB, error) { + p, err := auth.CreateDBImp[*model.Recipient](ctx, logger, pdb, enforcer, mservice.Organizations, db) + if err != nil { + return nil, err + } + + createEmpty := func() *model.Recipient { + return &model.Recipient{} + } + + getArchivable := func(c *model.Recipient) model.Archivable { + return &c.ArchivableBase + } + + res := &RecipientDB{ + ProtectedDBImp: *p, + ArchivableDB: auth.NewArchivableDB( + p.DBImp, + p.DBImp.Logger, + enforcer, + createEmpty, + getArchivable, + ), + paymentMethodsRepo: repository.CreateMongoRepository(db, string(mservice.PaymentMethods)), + pmdb: pmdb, + } + return res, nil +} diff --git a/api/pkg/db/internal/mongo/recipientdb/list.go b/api/pkg/db/internal/mongo/recipientdb/list.go new file mode 100644 index 0000000..29c1fc7 --- /dev/null +++ b/api/pkg/db/internal/mongo/recipientdb/list.go @@ -0,0 +1,28 @@ +package recipientdb + +import ( + "context" + "errors" + + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" + mauth "github.com/tech/sendico/pkg/mutil/db/auth" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func (db *RecipientDB) List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Recipient, error) { + res, err := mauth.GetProtectedObjects[model.Recipient]( + ctx, + db.DBImp.Logger, + accountRef, organizationRef, model.ActionRead, + repository.OrgFilter(organizationRef), + cursor, + db.Enforcer, + db.DBImp.Repository, + ) + if errors.Is(err, merrors.ErrNoData) { + return []model.Recipient{}, nil + } + return res, err +} diff --git a/api/pkg/db/invitation/invitation.go b/api/pkg/db/invitation/invitation.go index 88e9caf..d1229b7 100644 --- a/api/pkg/db/invitation/invitation.go +++ b/api/pkg/db/invitation/invitation.go @@ -15,5 +15,5 @@ type DB interface { Decline(ctx context.Context, invitationRef primitive.ObjectID) error List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Invitation, error) DeleteCascade(ctx context.Context, accountRef, statusRef primitive.ObjectID) error - SetArchived(ctx context.Context, accountRef, organizationRef, statusRef primitive.ObjectID, archived, cascade bool) error + SetArchived(ctx context.Context, accountRef, organizationRef, invitationRef primitive.ObjectID, archived, cascade bool) error } diff --git a/api/pkg/db/paymethod/db.go b/api/pkg/db/paymethod/db.go new file mode 100644 index 0000000..163845a --- /dev/null +++ b/api/pkg/db/paymethod/db.go @@ -0,0 +1,15 @@ +package paymethod + +import ( + "context" + + "github.com/tech/sendico/pkg/auth" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type DB interface { + auth.ProtectedDB[*model.PaymentMethod] + SetArchived(ctx context.Context, accountRef, organizationRef, methodRef primitive.ObjectID, archived, cascade bool) error + List(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, cursor *model.ViewCursor) ([]model.PaymentMethod, error) +} diff --git a/api/pkg/db/recipient/db.go b/api/pkg/db/recipient/db.go new file mode 100644 index 0000000..3b8ad0a --- /dev/null +++ b/api/pkg/db/recipient/db.go @@ -0,0 +1,15 @@ +package recipient + +import ( + "context" + + "github.com/tech/sendico/pkg/auth" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type DB interface { + auth.ProtectedDB[*model.Recipient] + SetArchived(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, archived, cascade bool) error + List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Recipient, error) +} diff --git a/api/pkg/model/card.go b/api/pkg/model/card.go new file mode 100644 index 0000000..9019234 --- /dev/null +++ b/api/pkg/model/card.go @@ -0,0 +1,25 @@ +package model + +import ( + "fmt" + + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson" +) + +type CardPaymentData struct { + Pan string `bson:"pan" json:"pan"` + FirstName string `bson:"firstName" json:"firstName"` + LastName string `bson:"lastName" json:"lastName"` +} + +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 +} diff --git a/api/pkg/model/iban.go b/api/pkg/model/iban.go new file mode 100644 index 0000000..a729c4a --- /dev/null +++ b/api/pkg/model/iban.go @@ -0,0 +1,26 @@ +package model + +import ( + "fmt" + + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/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 +} diff --git a/api/pkg/model/payment.go b/api/pkg/model/payment.go new file mode 100644 index 0000000..67a345a --- /dev/null +++ b/api/pkg/model/payment.go @@ -0,0 +1,66 @@ +package model + +import ( + "encoding/json" + "fmt" + + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type PaymentType int + +const ( + PaymentTypeIban PaymentType = iota + PaymentTypeCard + PaymentTypeBankAccount + PaymentTypeWallet +) + +var paymentTypeToString = map[PaymentType]string{ + PaymentTypeIban: "iban", + PaymentTypeCard: "card", + PaymentTypeBankAccount: "bankAccount", + PaymentTypeWallet: "wallet", +} + +var paymentTypeFromString = map[string]PaymentType{ + "iban": PaymentTypeIban, + "card": PaymentTypeCard, + "bankAccount": PaymentTypeBankAccount, + "wallet": PaymentTypeWallet, +} + +func (t PaymentType) String() string { + if v, ok := paymentTypeToString[t]; ok { + return v + } + return "iban" +} + +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 PaymentMethod struct { + PermissionBound `bson:",inline" json:",inline"` + + RecipientRef primitive.ObjectID `bson:"recipientRef" json:"recipientRef"` + Type PaymentType `bson:"type" json:"type"` + IsActive bool `bson:"isActive" json:"isActive"` + Data bson.Raw `bson:"data" json:"data"` +} diff --git a/api/pkg/model/rba.go b/api/pkg/model/rba.go new file mode 100644 index 0000000..fc4f031 --- /dev/null +++ b/api/pkg/model/rba.go @@ -0,0 +1,31 @@ +package model + +import ( + "fmt" + + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson" +) + +type RussianBankAccountPaymentData struct { + RecipientName string `bson:"recipientName" json:"recipientName"` + 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"` +} + +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 +} diff --git a/api/pkg/model/recipient.go b/api/pkg/model/recipient.go new file mode 100644 index 0000000..3037cea --- /dev/null +++ b/api/pkg/model/recipient.go @@ -0,0 +1,106 @@ +package model + +import ( + "encoding/json" + "fmt" + + "github.com/tech/sendico/pkg/merrors" +) + +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 +} + +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"` +} diff --git a/api/pkg/model/wallet.go b/api/pkg/model/wallet.go new file mode 100644 index 0000000..cf46f14 --- /dev/null +++ b/api/pkg/model/wallet.go @@ -0,0 +1,25 @@ +package model + +import ( + "fmt" + + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/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 +} diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go index d1c4e5b..da9f5aa 100644 --- a/api/pkg/mservice/services.go +++ b/api/pkg/mservice/services.go @@ -33,9 +33,11 @@ const ( Notifications Type = "notifications" // Represents notifications sent to users Organizations Type = "organizations" // Represents organizations in the system Payments Type = "payments" // Represents payments service + PaymentMethods Type = "payment_methods" // Represents payment methods service Permissions Type = "permissions" // Represents permissiosns service Policies Type = "policies" // Represents access control policies PolicyAssignements Type = "policy_assignments" // Represents policy assignments database + Recipients Type = "recipients" // Represents payment recipients RefreshTokens Type = "refresh_tokens" // Represents refresh tokens for authentication Roles Type = "roles" // Represents roles in access control Storage Type = "storage" // Represents statuses of tasks or projects diff --git a/api/server/interface/services/paymethod/paymethod.go b/api/server/interface/services/paymethod/paymethod.go new file mode 100644 index 0000000..299046d --- /dev/null +++ b/api/server/interface/services/paymethod/paymethod.go @@ -0,0 +1,11 @@ +package paymethod + +import ( + "github.com/tech/sendico/pkg/mservice" + "github.com/tech/sendico/server/interface/api" + "github.com/tech/sendico/server/internal/server/paymethodsimp" +) + +func Create(a api.API) (mservice.MicroService, error) { + return paymethodsimp.CreateAPI(a) +} diff --git a/api/server/interface/services/recipient/recipient.go b/api/server/interface/services/recipient/recipient.go new file mode 100644 index 0000000..bca4044 --- /dev/null +++ b/api/server/interface/services/recipient/recipient.go @@ -0,0 +1,11 @@ +package recipient + +import ( + "github.com/tech/sendico/pkg/mservice" + "github.com/tech/sendico/server/interface/api" + recipientimp "github.com/tech/sendico/server/internal/server/paymethodsimp" +) + +func Create(a api.API) (mservice.MicroService, error) { + return recipientimp.CreateAPI(a) +} diff --git a/api/server/internal/api/api.go b/api/server/internal/api/api.go index 02301d5..52a2ab4 100644 --- a/api/server/internal/api/api.go +++ b/api/server/internal/api/api.go @@ -14,12 +14,14 @@ import ( "github.com/tech/sendico/server/interface/services/account" "github.com/tech/sendico/server/interface/services/confirmation" "github.com/tech/sendico/server/interface/services/invitation" + "github.com/tech/sendico/server/interface/services/ledger" "github.com/tech/sendico/server/interface/services/logo" "github.com/tech/sendico/server/interface/services/organization" + "github.com/tech/sendico/server/interface/services/paymethod" "github.com/tech/sendico/server/interface/services/permission" + "github.com/tech/sendico/server/interface/services/recipient" "github.com/tech/sendico/server/interface/services/site" "github.com/tech/sendico/server/interface/services/wallet" - "github.com/tech/sendico/server/interface/services/ledger" "go.uber.org/zap" ) @@ -87,6 +89,8 @@ func (a *APIImp) installServices() error { srvf = append(srvf, site.Create) srvf = append(srvf, wallet.Create) srvf = append(srvf, ledger.Create) + srvf = append(srvf, recipient.Create) + srvf = append(srvf, paymethod.Create) for _, v := range srvf { if err := a.addMicroservice(v); err != nil { diff --git a/api/server/internal/server/paymethodsimp/service.go b/api/server/internal/server/paymethodsimp/service.go new file mode 100644 index 0000000..38824d2 --- /dev/null +++ b/api/server/internal/server/paymethodsimp/service.go @@ -0,0 +1,46 @@ +package paymethodsimp + +import ( + "context" + + "github.com/tech/sendico/pkg/db/paymethod" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + eapi "github.com/tech/sendico/server/interface/api" + "github.com/tech/sendico/server/internal/server/papitemplate" + "go.uber.org/zap" +) + +type RecipientAPI struct { + papitemplate.ProtectedAPI[model.PaymentMethod] + db paymethod.DB +} + +func (a *RecipientAPI) Name() mservice.Type { + return mservice.Recipients +} + +func (a *RecipientAPI) Finish(_ context.Context) error { + return nil +} + +func CreateAPI(a eapi.API) (*RecipientAPI, error) { + dbFactory := func() (papitemplate.ProtectedDB[model.PaymentMethod], error) { + return a.DBFactory().NewPaymentMethodsDB() + } + + res := &RecipientAPI{} + + p, err := papitemplate.CreateAPI(a, dbFactory, mservice.Recipients, mservice.PaymentMethods) + if err != nil { + return nil, err + } + res.ProtectedAPI = *p.Build() + + if res.db, err = a.DBFactory().NewPaymentMethodsDB(); err != nil { + res.Logger.Warn("Failed to create payment methods database", zap.Error(err)) + return nil, err + } + + return res, nil +} diff --git a/api/server/internal/server/recipientimp/service.go b/api/server/internal/server/recipientimp/service.go new file mode 100644 index 0000000..d2b678b --- /dev/null +++ b/api/server/internal/server/recipientimp/service.go @@ -0,0 +1,46 @@ +package recipientimp + +import ( + "context" + + "github.com/tech/sendico/pkg/db/recipient" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + eapi "github.com/tech/sendico/server/interface/api" + "github.com/tech/sendico/server/internal/server/papitemplate" + "go.uber.org/zap" +) + +type RecipientAPI struct { + papitemplate.ProtectedAPI[model.Recipient] + db recipient.DB +} + +func (a *RecipientAPI) Name() mservice.Type { + return mservice.Recipients +} + +func (a *RecipientAPI) Finish(_ context.Context) error { + return nil +} + +func CreateAPI(a eapi.API) (*RecipientAPI, error) { + dbFactory := func() (papitemplate.ProtectedDB[model.Recipient], error) { + return a.DBFactory().NewRecipientsDB() + } + + res := &RecipientAPI{} + + p, err := papitemplate.CreateAPI(a, dbFactory, mservice.Organizations, mservice.Recipients) + if err != nil { + return nil, err + } + res.ProtectedAPI = *p.Build() + + if res.db, err = a.DBFactory().NewRecipientsDB(); err != nil { + res.Logger.Warn("Failed to create recipients database", zap.Error(err)) + return nil, err + } + + return res, nil +} diff --git a/frontend/pshared/lib/service/services.dart b/frontend/pshared/lib/service/services.dart index 3c327fc..aaddc8f 100644 --- a/frontend/pshared/lib/service/services.dart +++ b/frontend/pshared/lib/service/services.dart @@ -9,6 +9,9 @@ class Services { static const String storage = 'storage'; static const String chainWallets = 'chain_wallets'; + static const String recipients = 'recipients'; + static const String paymentMethods = 'payment_methods'; + static const String amplitude = 'amplitude'; static const String clients = 'clients'; static const String logo = 'logo';