new payment methods service
This commit is contained in:
@@ -7,11 +7,15 @@ import (
|
||||
"github.com/tech/sendico/payments/storage"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/payments/storage/mongo/store"
|
||||
quotestorage "github.com/tech/sendico/payments/storage/quote"
|
||||
quotemongo "github.com/tech/sendico/payments/storage/quote/mongo"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// Store implements storage.Repository backed by MongoDB.
|
||||
@@ -20,13 +24,20 @@ type Store struct {
|
||||
ping func(context.Context) error
|
||||
|
||||
payments storage.PaymentsStore
|
||||
quotes storage.QuotesStore
|
||||
methods storage.PaymentMethodsStore
|
||||
quotes quotestorage.QuotesStore
|
||||
routes storage.RoutesStore
|
||||
plans storage.PlanTemplatesStore
|
||||
}
|
||||
|
||||
type paymentMethodsConfig struct {
|
||||
enforcer auth.Enforcer
|
||||
permissionRef bson.ObjectID
|
||||
}
|
||||
|
||||
type options struct {
|
||||
quoteRetention time.Duration
|
||||
quoteRetention time.Duration
|
||||
paymentMethodsAuth *paymentMethodsConfig
|
||||
}
|
||||
|
||||
// Option configures the Mongo-backed payments repository.
|
||||
@@ -39,6 +50,16 @@ func WithQuoteRetention(retention time.Duration) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithPaymentMethodsAuth enables the payment-methods store and permission checks.
|
||||
func WithPaymentMethodsAuth(enforcer auth.Enforcer, permissionRef bson.ObjectID) Option {
|
||||
return func(opts *options) {
|
||||
opts.paymentMethodsAuth = &paymentMethodsConfig{
|
||||
enforcer: enforcer,
|
||||
permissionRef: permissionRef,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New constructs a Mongo-backed payments repository from a Mongo connection.
|
||||
func New(logger mlogger.Logger, conn *db.MongoConnection, opts ...Option) (*Store, error) {
|
||||
if conn == nil {
|
||||
@@ -48,11 +69,22 @@ func New(logger mlogger.Logger, conn *db.MongoConnection, opts ...Option) (*Stor
|
||||
quotesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentQuoteRecord{}).Collection())
|
||||
routesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentRoute{}).Collection())
|
||||
plansRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentPlanTemplate{}).Collection())
|
||||
return NewWithRepository(logger, conn.Ping, paymentsRepo, quotesRepo, routesRepo, plansRepo, opts...)
|
||||
methodsRepo := repository.CreateMongoRepository(conn.Database(), mservice.PaymentMethods)
|
||||
|
||||
return newWithRepository(logger, conn.Ping, paymentsRepo, methodsRepo, quotesRepo, routesRepo, plansRepo, opts...)
|
||||
}
|
||||
|
||||
// NewWithRepository constructs a payments repository using the provided primitives.
|
||||
func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, paymentsRepo repository.Repository, quotesRepo repository.Repository, routesRepo repository.Repository, plansRepo repository.Repository, opts ...Option) (*Store, error) {
|
||||
return newWithRepository(logger, ping, paymentsRepo, nil, quotesRepo, routesRepo, plansRepo, opts...)
|
||||
}
|
||||
|
||||
func newWithRepository(
|
||||
logger mlogger.Logger,
|
||||
ping func(context.Context) error,
|
||||
paymentsRepo, methodsRepo, quotesRepo, routesRepo, plansRepo repository.Repository,
|
||||
opts ...Option,
|
||||
) (*Store, error) {
|
||||
if ping == nil {
|
||||
return nil, merrors.InvalidArgument("payments.storage.mongo: ping func is nil")
|
||||
}
|
||||
@@ -93,10 +125,30 @@ func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var methodsStore storage.PaymentMethodsStore
|
||||
if cfg.paymentMethodsAuth != nil {
|
||||
if methodsRepo == nil {
|
||||
return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods repository is nil")
|
||||
}
|
||||
if cfg.paymentMethodsAuth.enforcer == nil {
|
||||
return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods enforcer is nil")
|
||||
}
|
||||
if cfg.paymentMethodsAuth.permissionRef == bson.NilObjectID {
|
||||
return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods permission reference is required")
|
||||
}
|
||||
|
||||
methodsStore, err = store.NewPaymentMethods(childLogger, methodsRepo, cfg.paymentMethodsAuth.enforcer, cfg.paymentMethodsAuth.permissionRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result := &Store{
|
||||
logger: childLogger,
|
||||
ping: ping,
|
||||
payments: paymentsStore,
|
||||
methods: methodsStore,
|
||||
quotes: quotesRepoStore.Quotes(),
|
||||
routes: routesStore,
|
||||
plans: plansStore,
|
||||
@@ -118,8 +170,13 @@ func (s *Store) Payments() storage.PaymentsStore {
|
||||
return s.payments
|
||||
}
|
||||
|
||||
// PaymentMethods returns the payment-methods store.
|
||||
func (s *Store) PaymentMethods() storage.PaymentMethodsStore {
|
||||
return s.methods
|
||||
}
|
||||
|
||||
// Quotes returns the quotes store.
|
||||
func (s *Store) Quotes() storage.QuotesStore {
|
||||
func (s *Store) Quotes() quotestorage.QuotesStore {
|
||||
return s.quotes
|
||||
}
|
||||
|
||||
|
||||
235
api/payments/storage/mongo/store/payment_methods.go
Normal file
235
api/payments/storage/mongo/store/payment_methods.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/tech/sendico/payments/storage"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pkgmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
mauth "github.com/tech/sendico/pkg/mutil/db/auth"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type PaymentMethods struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
enforcer auth.Enforcer
|
||||
permissionRef bson.ObjectID
|
||||
}
|
||||
|
||||
// NewPaymentMethods constructs a Mongo-backed payment-methods store.
|
||||
func NewPaymentMethods(logger mlogger.Logger, repo repository.Repository, enforcer auth.Enforcer, permissionRef bson.ObjectID) (*PaymentMethods, error) {
|
||||
if repo == nil {
|
||||
return nil, merrors.InvalidArgument("paymentMethodsStore: repository is nil")
|
||||
}
|
||||
if enforcer == nil {
|
||||
return nil, merrors.InvalidArgument("paymentMethodsStore: enforcer is nil")
|
||||
}
|
||||
if permissionRef == bson.NilObjectID {
|
||||
return nil, merrors.InvalidArgument("paymentMethodsStore: permission reference is required")
|
||||
}
|
||||
|
||||
indexes := []*ri.Definition{
|
||||
{
|
||||
Keys: []ri.Key{
|
||||
{Field: "organizationRef", Sort: ri.Asc},
|
||||
{Field: "recipientRef", Sort: ri.Asc},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: []ri.Key{{Field: "recipientRef", Sort: ri.Asc}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, def := range indexes {
|
||||
if err := repo.CreateIndex(def); err != nil {
|
||||
logger.Error("failed to ensure payment methods index", zap.Error(err), zap.String("collection", repo.Collection()))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &PaymentMethods{
|
||||
logger: logger.Named("payment_methods"),
|
||||
repo: repo,
|
||||
enforcer: enforcer,
|
||||
permissionRef: permissionRef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) Create(ctx context.Context, accountRef, organizationRef bson.ObjectID, method *pkgmodel.PaymentMethod) error {
|
||||
if method == nil {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: nil payment method")
|
||||
}
|
||||
if accountRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
|
||||
}
|
||||
if organizationRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: organization_ref is required")
|
||||
}
|
||||
if method.RecipientRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
|
||||
}
|
||||
|
||||
if method.GetPermissionRef() == bson.NilObjectID {
|
||||
method.SetPermissionRef(p.permissionRef)
|
||||
}
|
||||
method.SetOrganizationRef(organizationRef)
|
||||
|
||||
allowed, err := p.enforcer.Enforce(ctx, method.GetPermissionRef(), accountRef, organizationRef, bson.NilObjectID, pkgmodel.ActionCreate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed {
|
||||
return merrors.AccessDenied(mservice.PaymentMethods, string(pkgmodel.ActionCreate), bson.NilObjectID)
|
||||
}
|
||||
|
||||
return p.repo.Insert(ctx, method, nil)
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) Get(ctx context.Context, accountRef, methodRef bson.ObjectID) (*pkgmodel.PaymentMethod, error) {
|
||||
if accountRef == bson.NilObjectID {
|
||||
return nil, merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
|
||||
}
|
||||
if methodRef == bson.NilObjectID {
|
||||
return nil, merrors.InvalidArgument("paymentMethodsStore: method_ref is required")
|
||||
}
|
||||
|
||||
if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := &pkgmodel.PaymentMethod{}
|
||||
if err := p.repo.Get(ctx, methodRef, method); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return method, nil
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) Update(ctx context.Context, accountRef bson.ObjectID, method *pkgmodel.PaymentMethod) error {
|
||||
if method == nil {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: nil payment method")
|
||||
}
|
||||
if accountRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
|
||||
}
|
||||
if method.GetID() == nil || method.GetID().IsZero() {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: method id is required")
|
||||
}
|
||||
|
||||
if err := p.enforceObject(ctx, accountRef, *method.GetID(), pkgmodel.ActionUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.repo.Update(ctx, method)
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) Delete(ctx context.Context, accountRef, methodRef bson.ObjectID) error {
|
||||
if accountRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
|
||||
}
|
||||
if methodRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: method_ref is required")
|
||||
}
|
||||
|
||||
if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionDelete); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.repo.Delete(ctx, methodRef)
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) DeleteCascade(ctx context.Context, accountRef, methodRef bson.ObjectID) error {
|
||||
return p.Delete(ctx, accountRef, methodRef)
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) SetArchived(ctx context.Context, accountRef, _ bson.ObjectID, methodRef bson.ObjectID, archived, _ bool) error {
|
||||
if accountRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
|
||||
}
|
||||
if methodRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: method_ref is required")
|
||||
}
|
||||
|
||||
if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patch := repository.Patch().Set(repository.Field("isArchived"), archived)
|
||||
return p.repo.Patch(ctx, methodRef, patch)
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) List(ctx context.Context, accountRef, organizationRef, recipientRef bson.ObjectID, cursor *pkgmodel.ViewCursor) ([]pkgmodel.PaymentMethod, error) {
|
||||
if accountRef == bson.NilObjectID {
|
||||
return nil, merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
|
||||
}
|
||||
if organizationRef == bson.NilObjectID {
|
||||
return nil, merrors.InvalidArgument("paymentMethodsStore: organization_ref is required")
|
||||
}
|
||||
if recipientRef == bson.NilObjectID {
|
||||
return nil, merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
|
||||
}
|
||||
|
||||
items, err := mauth.GetProtectedObjects[pkgmodel.PaymentMethod](
|
||||
ctx,
|
||||
p.logger,
|
||||
accountRef,
|
||||
organizationRef,
|
||||
pkgmodel.ActionRead,
|
||||
repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)),
|
||||
cursor,
|
||||
p.enforcer,
|
||||
p.repo,
|
||||
)
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return []pkgmodel.PaymentMethod{}, nil
|
||||
}
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) SetArchivedByRecipient(ctx context.Context, recipientRef bson.ObjectID, archived bool) (int, error) {
|
||||
if recipientRef == bson.NilObjectID {
|
||||
return 0, merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
|
||||
}
|
||||
|
||||
filter := repository.Filter("recipientRef", recipientRef)
|
||||
patch := repository.Patch().Set(repository.Field("isArchived"), archived)
|
||||
return p.repo.PatchMany(ctx, filter, patch)
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) DeleteByRecipient(ctx context.Context, recipientRef bson.ObjectID) error {
|
||||
if recipientRef == bson.NilObjectID {
|
||||
return merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
|
||||
}
|
||||
return p.repo.DeleteMany(ctx, repository.Filter("recipientRef", recipientRef))
|
||||
}
|
||||
|
||||
func (p *PaymentMethods) enforceObject(ctx context.Context, accountRef, methodRef bson.ObjectID, action pkgmodel.Action) error {
|
||||
refs, err := p.repo.ListPermissionBound(ctx, repository.IDFilter(methodRef))
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(refs) == 0 {
|
||||
return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef)
|
||||
}
|
||||
|
||||
allowed, err := p.enforcer.Enforce(ctx, refs[0].GetPermissionRef(), accountRef, refs[0].GetOrganizationRef(), methodRef, action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed {
|
||||
return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ storage.PaymentMethodsStore = (*PaymentMethods)(nil)
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
quotestorage "github.com/tech/sendico/payments/storage/quote"
|
||||
pkgmodel "github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
@@ -40,6 +41,7 @@ var (
|
||||
type Repository interface {
|
||||
Ping(ctx context.Context) error
|
||||
Payments() PaymentsStore
|
||||
PaymentMethods() PaymentMethodsStore
|
||||
Quotes() quotestorage.QuotesStore
|
||||
Routes() RoutesStore
|
||||
PlanTemplates() PlanTemplatesStore
|
||||
@@ -55,8 +57,19 @@ type PaymentsStore interface {
|
||||
List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error)
|
||||
}
|
||||
|
||||
// Deprecated: use quote/storage.QuotesStore.
|
||||
type QuotesStore = quotestorage.QuotesStore
|
||||
// PaymentMethodsStore manages recipient-linked payment methods.
|
||||
type PaymentMethodsStore interface {
|
||||
Create(ctx context.Context, accountRef, organizationRef bson.ObjectID, method *pkgmodel.PaymentMethod) error
|
||||
Get(ctx context.Context, accountRef, methodRef bson.ObjectID) (*pkgmodel.PaymentMethod, error)
|
||||
Update(ctx context.Context, accountRef bson.ObjectID, method *pkgmodel.PaymentMethod) error
|
||||
Delete(ctx context.Context, accountRef, methodRef bson.ObjectID) error
|
||||
DeleteCascade(ctx context.Context, accountRef, methodRef bson.ObjectID) error
|
||||
SetArchived(ctx context.Context, accountRef, organizationRef, methodRef bson.ObjectID, archived, cascade bool) error
|
||||
List(ctx context.Context, accountRef, organizationRef, recipientRef bson.ObjectID, cursor *pkgmodel.ViewCursor) ([]pkgmodel.PaymentMethod, error)
|
||||
|
||||
SetArchivedByRecipient(ctx context.Context, recipientRef bson.ObjectID, archived bool) (int, error)
|
||||
DeleteByRecipient(ctx context.Context, recipientRef bson.ObjectID) error
|
||||
}
|
||||
|
||||
// RoutesStore manages allowed routing transitions.
|
||||
type RoutesStore interface {
|
||||
|
||||
Reference in New Issue
Block a user