Documentation
¶
Overview ¶
Package mongo provides a MongoDB Store implementation for Ledger using Grove ORM.
Index ¶
- Variables
- type Store
- func (s *Store) AdvanceSubscriptionPeriod(ctx context.Context, subID id.SubscriptionID, from, start, end, at time.Time) (bool, error)
- func (s *Store) Aggregate(ctx context.Context, tenantID, appID, featureKey string, period plan.Period) (int64, error)
- func (s *Store) AggregateMulti(ctx context.Context, tenantID, appID string, featureKeys []string, ...) (map[string]int64, error)
- func (s *Store) ApplyCoupon(ctx context.Context, subID id.SubscriptionID, couponID id.CouponID) error
- func (s *Store) ArchiveFeature(ctx context.Context, featureID id.FeatureID) error
- func (s *Store) ArchivePlan(ctx context.Context, planID id.PlanID) error
- func (s *Store) CancelSubscription(ctx context.Context, subID id.SubscriptionID, immediately bool) (time.Time, error)
- func (s *Store) ChangeSubscriptionPlan(ctx context.Context, subID id.SubscriptionID, planID id.PlanID, ...) (bool, error)
- func (s *Store) Close() error
- func (s *Store) CreateCoupon(ctx context.Context, c *coupon.Coupon) error
- func (s *Store) CreateFeature(ctx context.Context, f *feature.Feature) error
- func (s *Store) CreateInvoice(ctx context.Context, inv *invoice.Invoice) error
- func (s *Store) CreatePlan(ctx context.Context, p *plan.Plan) error
- func (s *Store) CreateSubscription(ctx context.Context, sub *subscription.Subscription) error
- func (s *Store) DB() *grove.DB
- func (s *Store) DeleteCoupon(ctx context.Context, couponID id.CouponID) error
- func (s *Store) DeleteFeature(ctx context.Context, featureID id.FeatureID) error
- func (s *Store) DeletePlan(ctx context.Context, planID id.PlanID) error
- func (s *Store) EnactSubscriptionCancel(ctx context.Context, subID id.SubscriptionID, at time.Time) (bool, error)
- func (s *Store) EndSubscriptionTrial(ctx context.Context, subID id.SubscriptionID, at time.Time) (bool, error)
- func (s *Store) GetActiveSubscription(ctx context.Context, tenantID, appID string) (*subscription.Subscription, error)
- func (s *Store) GetCached(ctx context.Context, tenantID, appID, featureKey string) (*entitlement.Result, error)
- func (s *Store) GetCoupon(ctx context.Context, code, appID string) (*coupon.Coupon, error)
- func (s *Store) GetCouponByID(ctx context.Context, couponID id.CouponID) (*coupon.Coupon, error)
- func (s *Store) GetFeature(ctx context.Context, featureID id.FeatureID) (*feature.Feature, error)
- func (s *Store) GetFeatureByKey(ctx context.Context, key, appID string) (*feature.Feature, error)
- func (s *Store) GetInvoice(ctx context.Context, invID id.InvoiceID) (*invoice.Invoice, error)
- func (s *Store) GetInvoiceByPeriod(ctx context.Context, tenantID, appID string, periodStart, periodEnd time.Time) (*invoice.Invoice, error)
- func (s *Store) GetPlan(ctx context.Context, planID id.PlanID) (*plan.Plan, error)
- func (s *Store) GetPlanBySlug(ctx context.Context, slug, appID string) (*plan.Plan, error)
- func (s *Store) GetSubscription(ctx context.Context, subID id.SubscriptionID) (*subscription.Subscription, error)
- func (s *Store) IncrementCouponRedemptions(ctx context.Context, couponID id.CouponID) error
- func (s *Store) IngestBatch(ctx context.Context, events []*meter.UsageEvent) error
- func (s *Store) Invalidate(ctx context.Context, tenantID, appID string) error
- func (s *Store) InvalidateFeature(ctx context.Context, tenantID, appID, featureKey string) error
- func (s *Store) ListAppliedCoupons(ctx context.Context, subID id.SubscriptionID) ([]*coupon.Coupon, error)
- func (s *Store) ListCoupons(ctx context.Context, appID string, opts coupon.ListOpts) ([]*coupon.Coupon, error)
- func (s *Store) ListDueSubscriptions(ctx context.Context, opts subscription.DueOpts) ([]*subscription.Subscription, error)
- func (s *Store) ListFeatures(ctx context.Context, appID string, opts feature.ListOpts) ([]*feature.Feature, error)
- func (s *Store) ListGlobalFeatures(ctx context.Context, opts feature.ListOpts) ([]*feature.Feature, error)
- func (s *Store) ListInvoices(ctx context.Context, tenantID, appID string, opts invoice.ListOpts) ([]*invoice.Invoice, error)
- func (s *Store) ListOverdueInvoices(ctx context.Context, opts invoice.OverdueOpts) ([]*invoice.Invoice, error)
- func (s *Store) ListPendingInvoices(ctx context.Context, appID string) ([]*invoice.Invoice, error)
- func (s *Store) ListPlans(ctx context.Context, appID string, opts plan.ListOpts) ([]*plan.Plan, error)
- func (s *Store) ListSubscriptions(ctx context.Context, tenantID, appID string, opts subscription.ListOpts) ([]*subscription.Subscription, error)
- func (s *Store) MarkInvoicePaid(ctx context.Context, invID id.InvoiceID, paidAt time.Time, paymentRef string) error
- func (s *Store) MarkInvoicePastDue(ctx context.Context, invID id.InvoiceID, at time.Time) (bool, error)
- func (s *Store) MarkInvoiceVoided(ctx context.Context, invID id.InvoiceID, reason string) error
- func (s *Store) Migrate(ctx context.Context) error
- func (s *Store) PauseSubscription(ctx context.Context, subID id.SubscriptionID, at time.Time) (bool, error)
- func (s *Store) Ping(ctx context.Context) error
- func (s *Store) PurgeUsage(ctx context.Context, before time.Time) (int64, error)
- func (s *Store) QueryUsage(ctx context.Context, tenantID, appID string, opts meter.QueryOpts) ([]*meter.UsageEvent, error)
- func (s *Store) RedeemCoupon(ctx context.Context, subID id.SubscriptionID, couponID id.CouponID) error
- func (s *Store) ResumeSubscription(ctx context.Context, subID id.SubscriptionID, r subscription.Resume) (bool, error)
- func (s *Store) SetCached(ctx context.Context, tenantID, appID, featureKey string, ...) error
- func (s *Store) SetInvoiceProvider(ctx context.Context, invID id.InvoiceID, providerID, providerName string) error
- func (s *Store) SetSubscriptionProvider(ctx context.Context, subID id.SubscriptionID, providerID, providerName string) error
- func (s *Store) UpdateCoupon(ctx context.Context, c *coupon.Coupon) error
- func (s *Store) UpdateFeature(ctx context.Context, f *feature.Feature) error
- func (s *Store) UpdateInvoice(ctx context.Context, inv *invoice.Invoice) error
- func (s *Store) UpdatePlan(ctx context.Context, p *plan.Plan) error
- func (s *Store) UpdateSubscription(ctx context.Context, sub *subscription.Subscription) error
Constants ¶
This section is empty.
Variables ¶
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 (*Store) AdvanceSubscriptionPeriod ¶
func (*Store) AggregateMulti ¶
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 (*Store) ArchivePlan ¶
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 (*Store) CreateCoupon ¶
func (*Store) CreateFeature ¶
func (*Store) CreateInvoice ¶
func (*Store) CreateSubscription ¶
func (s *Store) CreateSubscription(ctx context.Context, sub *subscription.Subscription) error
func (*Store) DeleteCoupon ¶
func (*Store) DeleteFeature ¶
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 (*Store) GetActiveSubscription ¶
func (s *Store) GetActiveSubscription(ctx context.Context, tenantID, appID string) (*subscription.Subscription, error)
func (*Store) GetCouponByID ¶
func (*Store) GetFeature ¶
func (*Store) GetFeatureByKey ¶
func (*Store) GetInvoice ¶
func (*Store) GetInvoiceByPeriod ¶
func (*Store) GetPlanBySlug ¶
func (*Store) GetSubscription ¶
func (s *Store) GetSubscription(ctx context.Context, subID id.SubscriptionID) (*subscription.Subscription, error)
func (*Store) IncrementCouponRedemptions ¶
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 (*Store) Invalidate ¶
func (*Store) InvalidateFeature ¶
func (*Store) ListAppliedCoupons ¶
func (*Store) ListCoupons ¶
func (*Store) ListDueSubscriptions ¶
func (s *Store) ListDueSubscriptions(ctx context.Context, opts subscription.DueOpts) ([]*subscription.Subscription, error)
func (*Store) ListFeatures ¶
func (*Store) ListGlobalFeatures ¶
func (*Store) ListInvoices ¶
func (*Store) ListOverdueInvoices ¶
func (*Store) ListPendingInvoices ¶
func (*Store) ListSubscriptions ¶
func (s *Store) ListSubscriptions(ctx context.Context, tenantID, appID string, opts subscription.ListOpts) ([]*subscription.Subscription, error)
func (*Store) MarkInvoicePaid ¶
func (*Store) MarkInvoicePastDue ¶
func (*Store) MarkInvoiceVoided ¶
func (*Store) Migrate ¶
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 (*Store) PurgeUsage ¶
func (*Store) QueryUsage ¶
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) SetInvoiceProvider ¶
func (*Store) SetSubscriptionProvider ¶
func (*Store) UpdateCoupon ¶
func (*Store) UpdateFeature ¶
func (*Store) UpdateInvoice ¶
func (*Store) UpdateSubscription ¶
func (s *Store) UpdateSubscription(ctx context.Context, sub *subscription.Subscription) error