mongo

package
v1.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package mongo provides a MongoDB Store implementation for Ledger using Grove ORM.

Index

Constants

This section is empty.

Variables

View Source
var Migrations = migrate.NewGroup("ledger")

Migrations is the grove migration group for the Ledger mongo store.

Functions

This section is empty.

Types

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store implements store.Store using MongoDB via Grove ORM.

func New

func New(db *grove.DB) *Store

New creates a new MongoDB store backed by Grove ORM.

func (*Store) AdvanceSubscriptionPeriod

func (s *Store) AdvanceSubscriptionPeriod(ctx context.Context, subID id.SubscriptionID, from, start, end, at time.Time) (bool, error)

func (*Store) Aggregate

func (s *Store) Aggregate(ctx context.Context, tenantID, appID, featureKey string, period plan.Period) (int64, error)

func (*Store) AggregateMulti

func (s *Store) AggregateMulti(ctx context.Context, tenantID, appID string, featureKeys []string, period plan.Period) (map[string]int64, error)

func (*Store) ApplyCoupon

func (s *Store) ApplyCoupon(ctx context.Context, subID id.SubscriptionID, couponID id.CouponID) error

ApplyCoupon is a low-level operation kept for callers and tests that need it separately. Engine code redeems through RedeemCoupon, which records the application and increments the redemption count as one unit.

func (*Store) ArchiveFeature

func (s *Store) ArchiveFeature(ctx context.Context, featureID id.FeatureID) error

func (*Store) ArchivePlan

func (s *Store) ArchivePlan(ctx context.Context, planID id.PlanID) error

func (*Store) CancelSubscription

func (s *Store) CancelSubscription(ctx context.Context, subID id.SubscriptionID, immediately bool) (time.Time, error)

CancelSubscription writes through the driver's collection and not grove's update builder: a scheduled cancel copies current_period_end into cancel_at with a pipeline update, which the builder cannot express, and FindOneAndUpdate hands back the stored cancel_at in the same operation. So grove's operation hooks do not see this write, as with EnactSubscriptionCancel.

func (*Store) ChangeSubscriptionPlan

func (s *Store) ChangeSubscriptionPlan(ctx context.Context, subID id.SubscriptionID, planID id.PlanID, quantity map[string]int64) (bool, error)

func (*Store) Close

func (s *Store) Close() error

Close closes the database connection.

func (*Store) CreateCoupon

func (s *Store) CreateCoupon(ctx context.Context, c *coupon.Coupon) error

func (*Store) CreateFeature

func (s *Store) CreateFeature(ctx context.Context, f *feature.Feature) error

func (*Store) CreateInvoice

func (s *Store) CreateInvoice(ctx context.Context, inv *invoice.Invoice) error

func (*Store) CreatePlan

func (s *Store) CreatePlan(ctx context.Context, p *plan.Plan) error

func (*Store) CreateSubscription

func (s *Store) CreateSubscription(ctx context.Context, sub *subscription.Subscription) error

func (*Store) DB

func (s *Store) DB() *grove.DB

DB returns the underlying grove database for direct access.

func (*Store) DeleteCoupon

func (s *Store) DeleteCoupon(ctx context.Context, couponID id.CouponID) error

func (*Store) DeleteFeature

func (s *Store) DeleteFeature(ctx context.Context, featureID id.FeatureID) error

func (*Store) DeletePlan

func (s *Store) DeletePlan(ctx context.Context, planID id.PlanID) error

func (*Store) EnactSubscriptionCancel

func (s *Store) EnactSubscriptionCancel(ctx context.Context, subID id.SubscriptionID, at time.Time) (bool, error)

EnactSubscriptionCancel copies cancel_at into canceled_at inside the update, with a pipeline, so the value written is the stored one and nothing is read first. Grove's update builder has no pipeline form, so this goes to the driver's collection directly and bypasses grove's operation hooks: a hook a host registers on the grove DB does not see this write, though it sees every other lifecycle transition.

func (*Store) EndSubscriptionTrial

func (s *Store) EndSubscriptionTrial(ctx context.Context, subID id.SubscriptionID, at time.Time) (bool, error)

func (*Store) GetActiveSubscription

func (s *Store) GetActiveSubscription(ctx context.Context, tenantID, appID string) (*subscription.Subscription, error)

func (*Store) GetCached

func (s *Store) GetCached(ctx context.Context, tenantID, appID, featureKey string) (*entitlement.Result, error)

func (*Store) GetCoupon

func (s *Store) GetCoupon(ctx context.Context, code, appID string) (*coupon.Coupon, error)

func (*Store) GetCouponByID

func (s *Store) GetCouponByID(ctx context.Context, couponID id.CouponID) (*coupon.Coupon, error)

func (*Store) GetFeature

func (s *Store) GetFeature(ctx context.Context, featureID id.FeatureID) (*feature.Feature, error)

func (*Store) GetFeatureByKey

func (s *Store) GetFeatureByKey(ctx context.Context, key, appID string) (*feature.Feature, error)

func (*Store) GetInvoice

func (s *Store) GetInvoice(ctx context.Context, invID id.InvoiceID) (*invoice.Invoice, error)

func (*Store) GetInvoiceByPeriod

func (s *Store) GetInvoiceByPeriod(ctx context.Context, tenantID, appID string, periodStart, periodEnd time.Time) (*invoice.Invoice, error)

func (*Store) GetPlan

func (s *Store) GetPlan(ctx context.Context, planID id.PlanID) (*plan.Plan, error)

func (*Store) GetPlanBySlug

func (s *Store) GetPlanBySlug(ctx context.Context, slug, appID string) (*plan.Plan, error)

func (*Store) GetSubscription

func (s *Store) GetSubscription(ctx context.Context, subID id.SubscriptionID) (*subscription.Subscription, error)

func (*Store) IncrementCouponRedemptions

func (s *Store) IncrementCouponRedemptions(ctx context.Context, couponID id.CouponID) error

IncrementCouponRedemptions is a low-level operation kept for callers and tests that need it separately. Engine code redeems through RedeemCoupon, which increments the count and records the application as one unit.

func (*Store) IngestBatch

func (s *Store) IngestBatch(ctx context.Context, events []*meter.UsageEvent) error

func (*Store) Invalidate

func (s *Store) Invalidate(ctx context.Context, tenantID, appID string) error

func (*Store) InvalidateFeature

func (s *Store) InvalidateFeature(ctx context.Context, tenantID, appID, featureKey string) error

func (*Store) ListAppliedCoupons

func (s *Store) ListAppliedCoupons(ctx context.Context, subID id.SubscriptionID) ([]*coupon.Coupon, error)

func (*Store) ListCoupons

func (s *Store) ListCoupons(ctx context.Context, appID string, opts coupon.ListOpts) ([]*coupon.Coupon, error)

func (*Store) ListDueSubscriptions

func (s *Store) ListDueSubscriptions(ctx context.Context, opts subscription.DueOpts) ([]*subscription.Subscription, error)

func (*Store) ListFeatures

func (s *Store) ListFeatures(ctx context.Context, appID string, opts feature.ListOpts) ([]*feature.Feature, error)

func (*Store) ListGlobalFeatures

func (s *Store) ListGlobalFeatures(ctx context.Context, opts feature.ListOpts) ([]*feature.Feature, error)

func (*Store) ListInvoices

func (s *Store) ListInvoices(ctx context.Context, tenantID, appID string, opts invoice.ListOpts) ([]*invoice.Invoice, error)

func (*Store) ListOverdueInvoices

func (s *Store) ListOverdueInvoices(ctx context.Context, opts invoice.OverdueOpts) ([]*invoice.Invoice, error)

func (*Store) ListPendingInvoices

func (s *Store) ListPendingInvoices(ctx context.Context, appID string) ([]*invoice.Invoice, error)

func (*Store) ListPlans

func (s *Store) ListPlans(ctx context.Context, appID string, opts plan.ListOpts) ([]*plan.Plan, error)

func (*Store) ListSubscriptions

func (s *Store) ListSubscriptions(ctx context.Context, tenantID, appID string, opts subscription.ListOpts) ([]*subscription.Subscription, error)

func (*Store) MarkInvoicePaid

func (s *Store) MarkInvoicePaid(ctx context.Context, invID id.InvoiceID, paidAt time.Time, paymentRef string) error

func (*Store) MarkInvoicePastDue

func (s *Store) MarkInvoicePastDue(ctx context.Context, invID id.InvoiceID, at time.Time) (bool, error)

func (*Store) MarkInvoiceVoided

func (s *Store) MarkInvoiceVoided(ctx context.Context, invID id.InvoiceID, reason string) error

func (*Store) Migrate

func (s *Store) Migrate(ctx context.Context) error

Migrate creates indexes for all ledger collections, then drops any index a new one has superseded (currently just the old sparse idempotency_key index on ledger_usage_events, replaced by a partial unique one).

It creates every new index BEFORE dropping anything superseded, never the other way around: verified live against MongoDB 7.0.41 that a partial index can be created while an old index with the same key pattern still exists, so there is no need to ever leave a collection with a moment of zero uniqueness protection. Dropping first would open exactly that gap - a duplicate key written into it would defeat the very check being installed, permanently, since the create that would normally catch it never runs against that data. A collection whose superseded index failed to get dropped (index-not-found aside) simply keeps both the old and new index; a collection whose NEW index failed to get created (most likely because existing documents already violate it, an E11000) is reported as a failure via the joined error, and its data is never touched.

An index listed in migrationUniqueIndexes is created in its own call, separate from the rest of its collection's indexes (which share one CreateMany): a duplicate-key failure building that one index must not also prevent the collection's other, unrelated indexes from landing.

Every collection is attempted, in a fixed (sorted) order, even after an earlier one fails: a bad collection must not leave every collection after it in the iteration order unindexed. All failures are returned together via errors.Join.

func (*Store) PauseSubscription

func (s *Store) PauseSubscription(ctx context.Context, subID id.SubscriptionID, at time.Time) (bool, error)

func (*Store) Ping

func (s *Store) Ping(ctx context.Context) error

Ping checks database connectivity.

func (*Store) PurgeUsage

func (s *Store) PurgeUsage(ctx context.Context, before time.Time) (int64, error)

func (*Store) QueryUsage

func (s *Store) QueryUsage(ctx context.Context, tenantID, appID string, opts meter.QueryOpts) ([]*meter.UsageEvent, error)

func (*Store) RedeemCoupon

func (s *Store) RedeemCoupon(ctx context.Context, subID id.SubscriptionID, couponID id.CouponID) error

RedeemCoupon records a coupon's application to a subscription and increments its redemption count.

This is compensating, not transactional: MongoDB multi-document transactions need a replica set, and the harness this store is tested against cannot assume one is available, so this method cannot open a session transaction the way the SQL backends open a database transaction. Instead it performs the two writes in sequence and undoes the first by hand when the second doesn't land as a success: insert the application row, then run the redemption-count increment as a single filtered update whose filter itself encodes the cap (`{max_redemptions: {$lte: 0}} OR {$expr: {$lt: [times_redeemed, max_redemptions]}}`), which MongoDB evaluates and applies atomically against that one document. A document update in MongoDB is always atomic per document, so the increment step alone is race-free the same way the SQL backends' conditional UPDATE is; what is not race-free is the gap between the two writes.

That gap has real, observable consequences, not just a theoretical one:

  • ListAppliedCoupons can return the application row before the count is incremented, or after it was inserted but before a failed increment's compensating delete has run.
  • An invoice generated for this subscription inside that window receives the coupon's discount even though the redemption was never counted against the cap.
  • A process crash inside the window (after the insert, before the increment or its compensating delete) leaves the application row permanently attached with no matching count, and nothing in this store detects or repairs that on its own.
  • An error from the increment step is itself ambiguous: MongoDB may have applied it server-side and failed to report success back (a dropped connection after the write, for instance), in which case the compensating delete below runs against a coupon whose count already moved, and this method still reports failure.

Given no replica set to transact against, this is the closest available approximation, and it is unverified against a live MongoDB server that also runs the rest of this store's conformance suite concurrently - the RedeemCoupon subtests have been run once against a real mongod in a scratch database (see the task report), but not under sustained concurrent load the way sqlite and postgres have via -race.

func (*Store) ResumeSubscription

func (s *Store) ResumeSubscription(ctx context.Context, subID id.SubscriptionID, r subscription.Resume) (bool, error)

ResumeSubscription moves cancel_at with the period end when it equals the old end, which needs the stored values inside the update, so like EnactSubscriptionCancel it is a pipeline update on the driver's collection and bypasses grove's operation hooks.

func (*Store) SetCached

func (s *Store) SetCached(ctx context.Context, tenantID, appID, featureKey string, result *entitlement.Result, ttl time.Duration) error

func (*Store) SetInvoiceProvider

func (s *Store) SetInvoiceProvider(ctx context.Context, invID id.InvoiceID, providerID, providerName string) error

func (*Store) SetSubscriptionProvider

func (s *Store) SetSubscriptionProvider(ctx context.Context, subID id.SubscriptionID, providerID, providerName string) error

func (*Store) UpdateCoupon

func (s *Store) UpdateCoupon(ctx context.Context, c *coupon.Coupon) error

func (*Store) UpdateFeature

func (s *Store) UpdateFeature(ctx context.Context, f *feature.Feature) error

func (*Store) UpdateInvoice

func (s *Store) UpdateInvoice(ctx context.Context, inv *invoice.Invoice) error

func (*Store) UpdatePlan

func (s *Store) UpdatePlan(ctx context.Context, p *plan.Plan) error

func (*Store) UpdateSubscription

func (s *Store) UpdateSubscription(ctx context.Context, sub *subscription.Subscription) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL