new payment methods service

This commit is contained in:
Stephan D
2026-02-12 21:10:33 +01:00
parent b80dca0ce9
commit a862e27087
106 changed files with 3262 additions and 414 deletions

View File

@@ -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
}

View 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)

View File

@@ -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 {