Skip to content

refactor: move quantity snapshotting to line engine - #4749

Merged
turip merged 1 commit into
mainfrom
feat/move-quantity-snapshot-to-lineengine-reconstructed
Aug 12, 2026
Merged

refactor: move quantity snapshotting to line engine#4749
turip merged 1 commit into
mainfrom
feat/move-quantity-snapshot-to-lineengine-reconstructed

Conversation

@turip

@turip turip commented Jul 19, 2026

Copy link
Copy Markdown
Member

Summary

  • move quantity snapshotting into the legacy invoicing line engine
  • construct and inject one legacy line engine into billing and subscription sync
  • narrow the quantity snapshot dependency used by the invoice updater

Motivation

This aligns quantity snapshot ownership with the line engine and prepares the codebase for moving split-line-group handling out of the main billing path.

Stack

Reconstruction note

This draft reconstructs the original commit from #4746. The existing PRs and branches were intentionally left unchanged.

Validation

  • make test-nocache: 6,156 passed, 9 skipped on the original commit
  • fast Go lint: 0 issues on the original commit

Summary by CodeRabbit

  • New Features

    • Added a centralized legacy billing line engine for invoice line processing and quantity snapshots.
    • Improved quantity snapshot handling for metered and flat-priced invoice lines.
    • Added feature-meter resolution and validation during snapshot processing.
  • Refactor

    • Billing and subscription synchronization now share the configured line engine.
    • Removed direct quantity snapshot responsibilities from the billing service.

Greptile Summary

This PR moves quantity snapshot logic from the billing service into the legacy lineengine.Engine, making the engine self-contained for all standard-invoice line processing. A single Engine instance is constructed in NewBillingRegistry and injected into both the billing service and the subscription-sync service, replacing the previous circular dependency where the engine called back into the billing service's SnapshotLineQuantity via the QuantitySnapshotter interface.

  • SnapshotLineQuantities / SnapshotLineQuantity and related helpers (resolveFeatureMeters, featureMetersErrorWrapper, getFeatureUsage) are moved from billing/service to billing/lineengine as methods on Engine, and the QuantitySnapshotter interface is removed from stdinvoice.go.
  • The InvoiceLineService interface (and SnapshotLineQuantity on billing.Service) is removed; invoiceupdater now declares its own narrow QuantitySnapshotter interface backed directly by the engine.
  • All call-sites (app wiring, test suites) are updated to construct the engine first and pass it through the config chain.

Confidence Score: 5/5

  • Safe to merge. The change is a mechanical relocation of quantity snapshotting logic with no behavioral differences — the engine now owns what the billing service previously delegated back to itself through an interface.
  • All snapshotting logic is moved faithfully: resolveFeatureMeters, featureMetersErrorWrapper, getFeatureUsage, and the parallel-snapshot loop are identical to the originals. The workerCount <= 0 guard is correctly dropped (Config.Validate now enforces ≥ 1). The single Engine instance is constructed before both the billing service and the subscription-sync service, so there are no ordering or aliasing issues. The test suites are updated consistently across all call-sites and the PR reports 6 156 tests passing.
  • No files require special attention.

Important Files Changed

Filename Overview
openmeter/billing/lineengine/engine.go Config extended with FeatureService, StreamingConnector, and MaxParallelQuantitySnapshots; Validate() now collects all errors; engine struct drops quantitySnapshotter field. Clean and correct.
openmeter/billing/lineengine/quantitysnapshot.go Quantity snapshotting logic moved from billing/service to Engine. resolveFeatureMeters, featureMetersErrorWrapper, getFeatureUsage, and parallel snapshotting are now on the engine. The workerCount <= 0 guard (previously flagged) is correctly removed since Validate() enforces >= 1.
openmeter/billing/lineengine/stdinvoice.go QuantitySnapshotter interface removed; Engine now directly calls e.SnapshotLineQuantities(). Clean simplification.
openmeter/billing/service/service.go streamingConnector and maxParallelQuantitySnapshots fields removed; LegacyBillingLineEngine injected and registered. Config.Validate() drops StreamingConnector and MaxParallelQuantitySnapshots checks. Engine creation moved out of New().
app/common/billing.go NewLegacyBillingLineEngine factory added; BillingRegistry now exposes the engine; engine threaded into billing service and subscription sync service. Wiring is consistent.
openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go QuantitySnapshotter interface introduced locally; Updater now receives it via Config and calls SnapshotLineQuantity on the engine rather than billing.Service. Config.Validate() uses errors.Join for completeness. Well-structured change.
openmeter/billing/worker/subscriptionsync/service/reconciler/reconciler.go LegacyBillingLineEngine threaded through; invoiceupdater.New now returns an error and is constructed with a Config. Error handled properly.
openmeter/billing/worker/subscriptionsync/service/service.go LegacyBillingLineEngine added to Config and validated; propagated to reconciler. Straightforward plumbing.
openmeter/billing/service.go InvoiceLineService interface (SnapshotLineQuantity) removed from billing.Service. Clean narrowing of the public contract.

Sequence Diagram

sequenceDiagram
    participant Registry as BillingRegistry
    participant Engine as LegacyBillingLineEngine
    participant BillingService as billing.Service
    participant SubSync as SubscriptionSyncService
    participant InvoiceUpdater as invoiceupdater.Updater

    Registry->>Engine: NewLegacyBillingLineEngine(adapter, rating, feature, streaming)
    Registry->>BillingService: newBillingService(legacyBillingLineEngine)
    BillingService->>BillingService: RegisterLineEngine(engine)
    Registry->>SubSync: NewBillingSubscriptionSyncService(legacyBillingLineEngine)
    SubSync->>InvoiceUpdater: "invoiceupdater.New(Config{QuantitySnapshotter: engine})"

    Note over Engine: OnCollectionCompleted
    Engine->>Engine: SnapshotLineQuantities()
    Engine->>Engine: resolveFeatureMeters()
    Engine->>Engine: snapshotLineQuantitiesInParallel()

    Note over InvoiceUpdater: updateMutableStandardInvoice / updateImmutableInvoice
    InvoiceUpdater->>Engine: SnapshotLineQuantity(input)
    Engine->>Engine: resolveFeatureMeters()
    Engine->>Engine: snapshotLineQuantity()
Loading

Reviews (3): Last reviewed commit: "refactor: move quantity snapshotting to ..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change moves quantity snapshotting into billinglineengine.Engine. Billing and subscription-sync services now receive a shared legacy engine. Registry and test setup construct and inject the engine with feature, streaming, rating, and snapshot-limit dependencies.

Changes

Billing line engine integration

Layer / File(s) Summary
Engine snapshotting and dependencies
openmeter/billing/lineengine/*
The engine now owns quantity snapshotting, resolves feature meters, queries streaming usage, and validates its required dependencies.
Billing service and registry wiring
app/common/billing.go, openmeter/billing/service.go, openmeter/billing/service/service.go, openmeter/billing/stdinvoiceline.go, openmeter/billing/service/stdinvoiceline.go
The registry constructs and exposes a legacy billing line engine. The billing service accepts that engine and no longer exposes the removed invoice-line snapshot interface.
Subscription-sync snapshot injection
openmeter/billing/worker/subscriptionsync/service/*
Subscription sync validates the engine dependency and injects it into invoiceupdater.Updater for mutable and immutable invoice updates.
Test environment wiring
test/*, openmeter/billing/worker/subscriptionsync/service/*_test.go, openmeter/server/server_test.go
Test environments construct shared engines and pass them to billing and subscription-sync services. The no-op snapshot method was removed.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BillingRegistry
  participant BillingService
  participant SubscriptionSyncService
  participant InvoiceUpdater
  participant LegacyBillingLineEngine
  BillingRegistry->>LegacyBillingLineEngine: construct engine
  BillingRegistry->>BillingService: inject engine
  BillingRegistry->>SubscriptionSyncService: inject engine
  SubscriptionSyncService->>InvoiceUpdater: configure snapshotter
  InvoiceUpdater->>LegacyBillingLineEngine: snapshot invoice line quantity
  LegacyBillingLineEngine-->>InvoiceUpdater: return updated line
Loading

Possibly related PRs

Suggested labels: release-note/misc, area/billing

Suggested reviewers: tothandras, chrisgacsal

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: moving quantity snapshotting into the line engine.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/move-quantity-snapshot-to-lineengine-reconstructed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment on lines 145 to 149
func (e *Engine) snapshotLineQuantitiesInParallel(ctx context.Context, customer billing.InvoiceCustomer, lines billing.StandardLines, featureMeters feature.FeatureMeters) error {
workerCount := e.maxParallelQuantitySnapshots
if workerCount <= 0 {
workerCount = 1
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The workerCount <= 0 guard is unreachable: Config.Validate() already rejects any MaxParallelQuantitySnapshots < 1, so e.maxParallelQuantitySnapshots is guaranteed to be ≥ 1 by the time this method is called. The fallback can be safely removed.

Suggested change
func (e *Engine) snapshotLineQuantitiesInParallel(ctx context.Context, customer billing.InvoiceCustomer, lines billing.StandardLines, featureMeters feature.FeatureMeters) error {
workerCount := e.maxParallelQuantitySnapshots
if workerCount <= 0 {
workerCount = 1
}
func (e *Engine) snapshotLineQuantitiesInParallel(ctx context.Context, customer billing.InvoiceCustomer, lines billing.StandardLines, featureMeters feature.FeatureMeters) error {
workerCount := e.maxParallelQuantitySnapshots
Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/lineengine/quantitysnapshot.go
Line: 145-149

Comment:
The `workerCount <= 0` guard is unreachable: `Config.Validate()` already rejects any `MaxParallelQuantitySnapshots < 1`, so `e.maxParallelQuantitySnapshots` is guaranteed to be ≥ 1 by the time this method is called. The fallback can be safely removed.

```suggestion
func (e *Engine) snapshotLineQuantitiesInParallel(ctx context.Context, customer billing.InvoiceCustomer, lines billing.StandardLines, featureMeters feature.FeatureMeters) error {
	workerCount := e.maxParallelQuantitySnapshots
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

@turip
turip force-pushed the feat/split-get-subscription-calls-reconstructed branch from 97279e3 to 4ef880a Compare August 11, 2026 12:18
Base automatically changed from feat/split-get-subscription-calls-reconstructed to main August 11, 2026 13:30
@turip
turip force-pushed the feat/move-quantity-snapshot-to-lineengine-reconstructed branch from ef935d3 to 0b740b7 Compare August 11, 2026 13:46
@turip
turip marked this pull request as ready for review August 11, 2026 13:46
@turip
turip requested a review from a team as a code owner August 11, 2026 13:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
openmeter/billing/lineengine/engine.go (1)

32-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider collecting the config errors instead of returning on the first one.

The repo guideline for Validate() error is to accumulate field errors and return models.NewNillableGenericValidationError(errors.Join(errs...)). Here each check returns early, so a caller with two missing dependencies only sees the first one. The new invoiceupdater.Config.Validate in this same PR already uses the joined form, so aligning would keep the two configs consistent. Not blocking, since this matches the pre-existing style in this file.

As per coding guidelines: "In Validate() error, collect field errors and return models.NewNillableGenericValidationError(errors.Join(errs...)), preserving field context with wrapped errors."

♻️ Suggested aggregation
 func (c Config) Validate() error {
+	var errs []error
+
 	if c.SplitLineGroupAdapter == nil {
-		return fmt.Errorf("split line group adapter is required")
+		errs = append(errs, errors.New("split line group adapter is required"))
 	}
 
 	if c.RatingService == nil {
-		return fmt.Errorf("rating service is required")
+		errs = append(errs, errors.New("rating service is required"))
 	}
 
 	if c.FeatureService == nil {
-		return fmt.Errorf("feature service is required")
+		errs = append(errs, errors.New("feature service is required"))
 	}
 
 	if c.StreamingConnector == nil {
-		return fmt.Errorf("streaming connector is required")
+		errs = append(errs, errors.New("streaming connector is required"))
 	}
 
 	if c.MaxParallelQuantitySnapshots < 1 {
-		return fmt.Errorf("max parallel quantity snapshots must be greater than 0")
+		errs = append(errs, errors.New("max parallel quantity snapshots must be greater than 0"))
 	}
 
-	return nil
+	return models.NewNillableGenericValidationError(errors.Join(errs...))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/billing/lineengine/engine.go` around lines 32 - 54, Update
Config.Validate to collect all missing dependency and invalid
MaxParallelQuantitySnapshots errors instead of returning on the first failure.
Preserve field context by wrapping each validation error, then return
models.NewNillableGenericValidationError(errors.Join(errs...)); keep returning
nil when no errors are collected.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openmeter/billing/lineengine/engine.go`:
- Around line 32-54: Update Config.Validate to collect all missing dependency
and invalid MaxParallelQuantitySnapshots errors instead of returning on the
first failure. Preserve field context by wrapping each validation error, then
return models.NewNillableGenericValidationError(errors.Join(errs...)); keep
returning nil when no errors are collected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ea5f1c9c-5eac-479d-aca8-e83cbbc5791d

📥 Commits

Reviewing files that changed from the base of the PR and between 9d61ec8 and 0b740b7.

📒 Files selected for processing (19)
  • app/common/billing.go
  • openmeter/billing/lineengine/engine.go
  • openmeter/billing/lineengine/quantitysnapshot.go
  • openmeter/billing/lineengine/stdinvoice.go
  • openmeter/billing/service.go
  • openmeter/billing/service/service.go
  • openmeter/billing/service/stdinvoiceline.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/base_test.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/reconciler.go
  • openmeter/billing/worker/subscriptionsync/service/service.go
  • openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go
  • openmeter/server/server_test.go
  • test/app/testenv.go
  • test/billing/subscription_test.go
  • test/billing/suite.go
  • test/customer/testenv.go
  • test/subscription/framework_test.go
💤 Files with no reviewable changes (4)
  • openmeter/billing/service/stdinvoiceline.go
  • openmeter/server/server_test.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/service.go

@turip
turip force-pushed the feat/move-quantity-snapshot-to-lineengine-reconstructed branch from 0b740b7 to e105b4c Compare August 11, 2026 14:00
@borosr borosr added release-note/misc Miscellaneous changes area/billing labels Aug 11, 2026
@turip
turip merged commit 750c164 into main Aug 12, 2026
28 of 31 checks passed
@turip
turip deleted the feat/move-quantity-snapshot-to-lineengine-reconstructed branch August 12, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants