package store import ( "context" "errors" "time" "github.com/tech/sendico/billing/fees/storage" "github.com/tech/sendico/billing/fees/storage/model" "github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository/builder" ri "github.com/tech/sendico/pkg/db/repository/index" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" m "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" ) type plansStore struct { logger mlogger.Logger repo repository.Repository } // NewPlans constructs a Mongo-backed PlansStore. func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, error) { repo := repository.CreateMongoRepository(db, mservice.FeePlans) // Index for organisation lookups. orgIndex := &ri.Definition{ Keys: []ri.Key{ {Field: m.OrganizationRefField, Sort: ri.Asc}, {Field: "effectiveFrom", Sort: ri.Desc}, }, } if err := repo.CreateIndex(orgIndex); err != nil { logger.Error("failed to ensure fee plan organization index", zap.Error(err)) return nil, err } // Unique index for plan versions (per organisation + effectiveFrom). uniqueIndex := &ri.Definition{ Keys: []ri.Key{ {Field: m.OrganizationRefField, Sort: ri.Asc}, {Field: "effectiveFrom", Sort: ri.Asc}, }, Unique: true, } if err := repo.CreateIndex(uniqueIndex); err != nil { logger.Error("failed to ensure fee plan uniqueness index", zap.Error(err)) return nil, err } return &plansStore{ logger: logger.Named("plans"), repo: repo, }, nil } func (p *plansStore) Create(ctx context.Context, plan *model.FeePlan) error { if plan == nil { return merrors.InvalidArgument("plansStore: nil fee plan") } if err := p.repo.Insert(ctx, plan, nil); err != nil { if errors.Is(err, merrors.ErrDataConflict) { return storage.ErrDuplicateFeePlan } p.logger.Warn("failed to create fee plan", zap.Error(err)) return err } return nil } func (p *plansStore) Update(ctx context.Context, plan *model.FeePlan) error { if plan == nil || plan.GetID() == nil || plan.GetID().IsZero() { return merrors.InvalidArgument("plansStore: invalid fee plan reference") } if err := p.repo.Update(ctx, plan); err != nil { p.logger.Warn("failed to update fee plan", zap.Error(err)) return err } return nil } func (p *plansStore) Get(ctx context.Context, planRef primitive.ObjectID) (*model.FeePlan, error) { if planRef.IsZero() { return nil, merrors.InvalidArgument("plansStore: zero plan reference") } result := &model.FeePlan{} if err := p.repo.Get(ctx, planRef, result); err != nil { if errors.Is(err, merrors.ErrNoData) { return nil, storage.ErrFeePlanNotFound } return nil, err } return result, nil } func (p *plansStore) GetActivePlan(ctx context.Context, orgRef primitive.ObjectID, at time.Time) (*model.FeePlan, error) { if orgRef.IsZero() { return nil, merrors.InvalidArgument("plansStore: zero organization reference") } limit := int64(1) query := repository.Query(). Filter(repository.OrgField(), orgRef). Filter(repository.Field("active"), true). Comparison(repository.Field("effectiveFrom"), builder.Lte, at). Sort(repository.Field("effectiveFrom"), false). Limit(&limit) query = query.And( repository.Query().Or( repository.Query().Filter(repository.Field("effectiveTo"), nil), repository.Query().Comparison(repository.Field("effectiveTo"), builder.Gte, at), ), ) var plan *model.FeePlan decoder := func(cursor *mongo.Cursor) error { target := &model.FeePlan{} if err := cursor.Decode(target); err != nil { return err } plan = target return nil } if err := p.repo.FindManyByFilter(ctx, query, decoder); err != nil { if errors.Is(err, merrors.ErrNoData) { return nil, storage.ErrFeePlanNotFound } return nil, err } if plan == nil { return nil, storage.ErrFeePlanNotFound } return plan, nil } var _ storage.PlansStore = (*plansStore)(nil)