Skip to content

feat(billing): add collected invoice line limit#4632

Merged
turip merged 6 commits into
mainfrom
feat/collected-invoice-line-limit
Jul 1, 2026
Merged

feat(billing): add collected invoice line limit#4632
turip merged 6 commits into
mainfrom
feat/collected-invoice-line-limit

Conversation

@turip

@turip turip commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Adds a billing feature switch for limiting how many pending invoice lines are collected into each standard invoice.

  • introduces billing.featureSwitches.maxLinesPerCollectedInvoice
  • keeps 0 as the default unlimited behavior
  • applies positive limits by earliest gathering-line service period start, with deterministic tie-breaks
  • wires the same limit through invoice collection and subscription sync post-sync invoicing

Notes

Filtering happens before line preparation, standard invoice creation, quantity snapshotting, and invoice calculation. Lines beyond the limit stay on the gathering invoice for a later collection pass.

Validation

go test -tags=dynamic ./app/config ./openmeter/billing ./openmeter/billing/service ./openmeter/billing/worker/collect ./openmeter/billing/worker/subscriptionsync/service ./app/common ./cmd/billing-worker ./cmd/jobs/internal
go vet -tags=dynamic ./app/config ./app/common ./openmeter/billing ./openmeter/billing/service ./openmeter/billing/worker/collect ./openmeter/billing/worker/subscriptionsync/service

Summary by CodeRabbit

  • New Features
    • Added billing.featureSwitches.maxLinesPerCollectedInvoice to cap invoice line collection during subscription processing.
    • Added maxLinesPerInvoice support to cap pending invoice line generation, including deterministic capped selection.
    • Invoice collection now propagates the MaxLinesPerInvoice configuration into pending line generation.
  • Bug Fixes
    • Reject negative line-limit values.
  • Documentation
    • Updated config.example.yaml with billing.featureSwitches.maxLinesPerCollectedInvoice.
  • Tests
    • Added/expanded tests for feature-switch validation and capped vs. unlimited collection behavior (including batching and early-exit cases).

@turip turip requested a review from a team as a code owner July 1, 2026 13:52
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a configurable cap for collected invoice lines, validates and documents the new setting, propagates it through billing wiring, and applies it to gathering and subscription-sync invoice batching.

Changes

Invoice line cap flow

Layer / File(s) Summary
Config and defaults
app/config/billing.go, app/config/billing_test.go, config.example.yaml
Adds MaxLinesPerCollectedInvoice to billing feature switches, validates it is non-negative, sets the default to 0, documents it, and tests validation.
Invoice options and feature flags
openmeter/billing/invoice.go, openmeter/billing/worker/subscriptionsync/service/service.go, app/common/billing.go, cmd/billing-worker/wire_gen.go, cmd/jobs/internal/wire_gen.go
Adds the invoice line cap option, exposes it on subscription sync feature flags, and threads the config through billing registry and collector setup.
Gathering line limit enforcement
openmeter/billing/service/gatheringinvoicependinglines.go, openmeter/billing/service/gatheringinvoicependinglines_test.go
Validates the cap, truncates gathered lines deterministically, and adds tests for the truncation helper.
Collector and sync batching
openmeter/billing/worker/collect/collect.go, openmeter/billing/worker/subscriptionsync/service/sync.go, openmeter/billing/worker/subscriptionsync/service/sync_unit_test.go
Stores and forwards the cap in the collector, changes subscription sync to batch repeated pending-line calls until exhausted or capped out, and updates tests.

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

Possibly related PRs

Suggested reviewers: tothandras, borbelyr-kong

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a limit for collected invoice lines in billing.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/collected-invoice-line-limit

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.

@turip turip added area/billing kind/feature New feature or request release-note/feature Release note: Exciting New Features labels Jul 1, 2026
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a configurable limit for collected invoice lines. The main changes are:

  • A new billing.featureSwitches.maxLinesPerCollectedInvoice setting.
  • Validation that rejects negative line limits.
  • Collector and subscription-sync wiring for the new setting.
  • Deterministic capped selection by service period and line ID.
  • Tests for config validation and capped line ordering.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
openmeter/billing/service/gatheringinvoicependinglines.go Adds max-line validation and deterministic capped selection before pending lines are prepared for billing.
openmeter/billing/worker/subscriptionsync/service/sync.go Passes the configured collected-line limit into post-sync pending-line invoicing.
app/config/billing.go Adds the feature-switch field, default, and negative-value validation.
app/common/billing.go Wires billing feature-switch configuration into the collector and subscription sync service.
openmeter/billing/worker/collect/collect.go Stores the configured max-line limit on the collector and passes it to invoice collection.

Reviews (10): Last reviewed commit: "test(billing): remove repeated sync coll..." | Re-trigger Greptile

Comment thread openmeter/billing/service/gatheringinvoicependinglines.go
Comment thread openmeter/billing/worker/subscriptionsync/service/sync.go

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/service/gatheringinvoicependinglines.go (1)

208-213: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Truncation can silently drop explicitly-requested lines.

gatherInScopeLines already validates (and filters down to) the caller's explicitly requested LinesToInclude before this point — those lines were confirmed billable. But limitGatheringLinesForInvoice is then applied unconditionally, so if the number of explicitly requested lines exceeds MaxLinesPerInvoice, some pre-validated, user-requested lines get silently dropped with no error surfaced. That breaks the guarantee that requesting specific billable lines results in them being billed.

Consider skipping the truncation when input.IncludePendingLines is present, or returning an explicit error if the requested set exceeds the limit.

🩹 Possible fix
-				inScopeLines = limitGatheringLinesForInvoice(inScopeLines, options.MaxLinesPerInvoice)
+				if !input.IncludePendingLines.IsPresent() {
+					inScopeLines = limitGatheringLinesForInvoice(inScopeLines, options.MaxLinesPerInvoice)
+				}
🤖 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/service/gatheringinvoicependinglines.go` around lines 208 -
213, The truncation in gatherInScopeLines is dropping explicitly requested
billable lines because limitGatheringLinesForInvoice is applied unconditionally
after LinesToInclude has already been validated. Update the logic in
gatheringinvoicependinglines.go around gatherInScopeLines so that explicit
requests via input.IncludePendingLines/LinesToInclude are not silently
truncated; either skip limitGatheringLinesForInvoice for requested lines or
return an error when the requested set exceeds options.MaxLinesPerInvoice. Keep
the fix localized to gatherInScopeLines and limitGatheringLinesForInvoice
handling.
🧹 Nitpick comments (3)
openmeter/billing/invoice.go (1)

482-484: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a short doc comment for MaxLinesPerInvoice.

PartialInvoiceLinesEnabled right below has a clear doc block explaining its semantics, but MaxLinesPerInvoice (0 = no limit, positive = cap) has none. A one-liner would keep the contract obvious to callers using WithMaxLinesPerInvoice.

As per coding guidelines: "Add docstrings to domain helpers when the name compresses important business semantics, and describe the observable business contract rather than implementation mechanics."

✏️ Suggested doc comment
 type InvoicePendingLinesOptions struct {
 	BypassCollectionAlignment bool
-	MaxLinesPerInvoice        int
+	// MaxLinesPerInvoice caps the number of pending lines collected into a single invoice.
+	// 0 means no limit.
+	MaxLinesPerInvoice int
🤖 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/invoice.go` around lines 482 - 484, Add a short doc comment
for MaxLinesPerInvoice in InvoicePendingLinesOptions to make its business
contract explicit for callers of WithMaxLinesPerInvoice; document that 0 means
no limit and positive values cap the number of lines per invoice, matching the
style used for PartialInvoiceLinesEnabled.

Source: Coding guidelines

openmeter/billing/worker/subscriptionsync/service/sync_unit_test.go (1)

14-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the empty-success loop exit.

Both tests exercise looping and the cap<=0 single-call path, but the len(invoices) == 0 termination branch in sync.go (a successful call returning zero invoices) isn't exercised anywhere. Worth adding a mock result like {invoices: []billing.StandardInvoice{}} to confirm the loop exits cleanly on that path too, since it's new behavior introduced by this PR.

🤖 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/worker/subscriptionsync/service/sync_unit_test.go` around
lines 14 - 44, Add a unit test in invoicePendingLines coverage that exercises
the successful zero-invoices termination path in Service.invoicePendingLines.
Use invoicePendingLinesBillingService with a mocked result returning an empty
invoices slice, then assert the loop exits cleanly with no error and only the
expected single call(s) to billingService. Reference the existing
TestInvoicePendingLinesCollectsCappedBatchesUntilEmpty / invoicePendingLines
setup to keep the new case aligned with the current batching behavior.

Source: Path instructions

openmeter/billing/worker/subscriptionsync/service/sync.go (1)

36-56: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a safety cap on the collection loop.

The loop has no upper bound — it depends entirely on the invariant that each successful InvoicePendingLines call consumes lines from the gathering pool so it eventually hits ErrInvoiceCreateNoLines or an empty result. That should hold today, but there's no backstop if that invariant is ever broken (bug, data inconsistency, future refactor) — the worker would spin forever on a single customer with no visibility.

A small pass-count guard would make this robust without changing normal-case behavior.

🛡️ Proposed safety-net for the collection loop
 	return span.Wrap(func(ctx context.Context) error {
-		for {
+		const maxCollectionPasses = 10_000 // generous upper bound; each pass consumes at least one line
+		for pass := 0; pass < maxCollectionPasses; pass++ {
 			invoices, err := s.billingService.InvoicePendingLines(
 				ctx,
 				billing.InvoicePendingLinesInput{
 					Customer: customer,
 				},
 				billing.WithPartialInvoiceLinesDisabled(),
 				billing.WithMaxLinesPerInvoice(s.featureFlags.MaxLinesPerCollectedInvoice),
 			)
 			if err != nil {
 				if errors.Is(err, billing.ErrInvoiceCreateNoLines) {
 					return nil
 				}

 				return err
 			}

 			if s.featureFlags.MaxLinesPerCollectedInvoice <= 0 || len(invoices) == 0 {
 				return nil
 			}
 		}
+		return fmt.Errorf("exceeded max collection passes for customer %s", customer.ID)
 	})
🤖 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/worker/subscriptionsync/service/sync.go` around lines 36 -
56, The collection loop in Sync should have a hard safety cap so it cannot spin
forever if InvoicePendingLines stops making progress. Update the looping logic
in Sync to track how many successful collection passes have run for a customer
and exit once a small maximum is reached, while preserving the existing exit
conditions for billing.ErrInvoiceCreateNoLines and empty invoices. Use the
existing Sync method and InvoicePendingLines call as the main points to locate
and adjust the loop.
🤖 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.

Inline comments:
In `@app/config/billing.go`:
- Around line 42-49: BillingFeatureSwitchesConfiguration.Validate currently
returns the raw errors.Join result, but it should follow the config package
validation contract. Update Validate in BillingFeatureSwitchesConfiguration to
wrap the joined errors with
models.NewNillableGenericValidationError(errors.Join(errs...)), keeping the
existing err collection logic intact.

In `@openmeter/billing/service/gatheringinvoicependinglines.go`:
- Around line 141-144: Wrap the new MaxLinesPerInvoice validation failure in
billing.ValidationError, matching the existing validation paths in
GatherInvoicePendingLines. In the options.MaxLinesPerInvoice check, return
billing.ValidationError{Err: ...} instead of a bare errors.New so downstream
handling treats it as a client input error, consistent with input.Validate() and
the namespace lockdown validation.

---

Outside diff comments:
In `@openmeter/billing/service/gatheringinvoicependinglines.go`:
- Around line 208-213: The truncation in gatherInScopeLines is dropping
explicitly requested billable lines because limitGatheringLinesForInvoice is
applied unconditionally after LinesToInclude has already been validated. Update
the logic in gatheringinvoicependinglines.go around gatherInScopeLines so that
explicit requests via input.IncludePendingLines/LinesToInclude are not silently
truncated; either skip limitGatheringLinesForInvoice for requested lines or
return an error when the requested set exceeds options.MaxLinesPerInvoice. Keep
the fix localized to gatherInScopeLines and limitGatheringLinesForInvoice
handling.

---

Nitpick comments:
In `@openmeter/billing/invoice.go`:
- Around line 482-484: Add a short doc comment for MaxLinesPerInvoice in
InvoicePendingLinesOptions to make its business contract explicit for callers of
WithMaxLinesPerInvoice; document that 0 means no limit and positive values cap
the number of lines per invoice, matching the style used for
PartialInvoiceLinesEnabled.

In `@openmeter/billing/worker/subscriptionsync/service/sync_unit_test.go`:
- Around line 14-44: Add a unit test in invoicePendingLines coverage that
exercises the successful zero-invoices termination path in
Service.invoicePendingLines. Use invoicePendingLinesBillingService with a mocked
result returning an empty invoices slice, then assert the loop exits cleanly
with no error and only the expected single call(s) to billingService. Reference
the existing TestInvoicePendingLinesCollectsCappedBatchesUntilEmpty /
invoicePendingLines setup to keep the new case aligned with the current batching
behavior.

In `@openmeter/billing/worker/subscriptionsync/service/sync.go`:
- Around line 36-56: The collection loop in Sync should have a hard safety cap
so it cannot spin forever if InvoicePendingLines stops making progress. Update
the looping logic in Sync to track how many successful collection passes have
run for a customer and exit once a small maximum is reached, while preserving
the existing exit conditions for billing.ErrInvoiceCreateNoLines and empty
invoices. Use the existing Sync method and InvoicePendingLines call as the main
points to locate and adjust the loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 561e8b1d-baa6-4167-96b0-fd88e52f45a2

📥 Commits

Reviewing files that changed from the base of the PR and between 91f1e5a and cac2664.

📒 Files selected for processing (13)
  • app/common/billing.go
  • app/config/billing.go
  • app/config/billing_test.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • config.example.yaml
  • openmeter/billing/invoice.go
  • openmeter/billing/service/gatheringinvoicependinglines.go
  • openmeter/billing/service/gatheringinvoicependinglines_test.go
  • openmeter/billing/worker/collect/collect.go
  • openmeter/billing/worker/subscriptionsync/service/service.go
  • openmeter/billing/worker/subscriptionsync/service/sync.go
  • openmeter/billing/worker/subscriptionsync/service/sync_unit_test.go

Comment thread app/config/billing.go Outdated
Comment thread openmeter/billing/service/gatheringinvoicependinglines.go

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/worker/subscriptionsync/service/sync.go (1)

30-63: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Heads up: a low cap + big customer could actually hit the 10,000-pass ceiling.

Nice safety net with maxInvoicePendingCollectionPasses, but worth thinking through the numbers: with MaxLinesPerCollectedInvoice set low (say 1) and a customer with a large volume of pending lines, this loop could genuinely need more than 10,000 round-trips — each one doing a full InvoicePendingLines call (invoice creation, calculation, DB writes). That's a real production failure mode (hard error surfaced to the caller), not just a theoretical one.

A couple of light-touch improvements that could help without a big design change:

  • Log a warning (with pass count + customer ID) once we're getting close to the ceiling, so ops has visibility before it turns into a hard failure.
  • Consider whether the constant should scale with (or at least be validated against) the configured MaxLinesPerCollectedInvoice value, since a smaller cap directly multiplies the number of passes needed.
💡 Sketch of a warning before hitting the ceiling
 		for pass := 0; pass < maxInvoicePendingCollectionPasses; pass++ {
+			if pass == maxInvoicePendingCollectionPasses-1 {
+				s.logger.Warn("approaching max invoice pending collection passes", "customer_id", customer.ID, "pass", pass)
+			}
 			invoices, err := s.billingService.InvoicePendingLines(
🤖 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/worker/subscriptionsync/service/sync.go` around lines 30 -
63, The loop in invoicePendingLines can legitimately approach the
maxInvoicePendingCollectionPasses ceiling when MaxLinesPerCollectedInvoice is
very low, so add a warning before the hard stop that includes the pass count and
customer ID for visibility. Use the existing invoicePendingLines method and its
tracing/logging context to emit the warning as the pass count nears the ceiling,
and consider validating or deriving maxInvoicePendingCollectionPasses from
featureFlags.MaxLinesPerCollectedInvoice so the cap reflects the configured
batch size.
🤖 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.

Outside diff comments:
In `@openmeter/billing/worker/subscriptionsync/service/sync.go`:
- Around line 30-63: The loop in invoicePendingLines can legitimately approach
the maxInvoicePendingCollectionPasses ceiling when MaxLinesPerCollectedInvoice
is very low, so add a warning before the hard stop that includes the pass count
and customer ID for visibility. Use the existing invoicePendingLines method and
its tracing/logging context to emit the warning as the pass count nears the
ceiling, and consider validating or deriving maxInvoicePendingCollectionPasses
from featureFlags.MaxLinesPerCollectedInvoice so the cap reflects the configured
batch size.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 637a352c-8ffd-418c-8b39-22c4de4cb65b

📥 Commits

Reviewing files that changed from the base of the PR and between cac2664 and b198122.

📒 Files selected for processing (6)
  • app/config/billing.go
  • openmeter/billing/invoice.go
  • openmeter/billing/service/gatheringinvoicependinglines.go
  • openmeter/billing/service/gatheringinvoicependinglines_test.go
  • openmeter/billing/worker/subscriptionsync/service/sync.go
  • openmeter/billing/worker/subscriptionsync/service/sync_unit_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • openmeter/billing/invoice.go
  • app/config/billing.go
  • openmeter/billing/service/gatheringinvoicependinglines.go
  • openmeter/billing/service/gatheringinvoicependinglines_test.go

Comment thread openmeter/billing/worker/subscriptionsync/service/sync.go Outdated
@turip turip force-pushed the feat/collected-invoice-line-limit branch from dd357c8 to c05acc7 Compare July 1, 2026 14:27
Comment thread openmeter/billing/worker/subscriptionsync/service/sync.go Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/worker/subscriptionsync/service/sync.go (1)

36-60: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Please bound the capped collection loop.

With MaxLinesPerCollectedInvoice > 0, this keeps collecting while every pass returns at least one invoice. A large backlog—or pending lines being added concurrently—can keep one subscription sync busy for an unbounded number of billing/database passes. Consider reintroducing a per-sync pass limit and leaving the rest for a later collection pass.

Possible shape
-		for pass := 0; ; pass++ {
+		for pass := 0; pass < maxInvoicePendingCollectionPasses; pass++ {
 			if pass > 0 && pass%10_000 == 0 && s.logger != nil {
 				s.logger.WarnContext(ctx, "invoice pending collection is still processing capped batches", "customer_id", customer.ID, "pass", pass)
 			}
@@
 			if s.featureFlags.MaxLinesPerCollectedInvoice <= 0 || len(invoices) == 0 {
 				return nil
 			}
 		}
+
+		if s.logger != nil {
+			s.logger.WarnContext(ctx, "invoice pending collection reached capped pass limit", "customer_id", customer.ID, "passes", maxInvoicePendingCollectionPasses)
+		}
+		return nil

As per path instructions, performance should be prioritized for message processing and database operations.

🤖 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/worker/subscriptionsync/service/sync.go` around lines 36 -
60, The capped collection loop in the subscription sync flow is unbounded when
`MaxLinesPerCollectedInvoice` stays positive and invoices keep being produced.
Update the loop in `Sync` to enforce a per-sync pass limit before repeatedly
calling `billingService.InvoicePendingLines`, and exit early so remaining work
is handled by a later run. Use the existing `pass` counter and the
`Sync`/`InvoicePendingLines` flow to locate the loop, and keep the existing
nil/error handling intact while adding the bound.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@openmeter/billing/worker/subscriptionsync/service/sync.go`:
- Around line 36-60: The capped collection loop in the subscription sync flow is
unbounded when `MaxLinesPerCollectedInvoice` stays positive and invoices keep
being produced. Update the loop in `Sync` to enforce a per-sync pass limit
before repeatedly calling `billingService.InvoicePendingLines`, and exit early
so remaining work is handled by a later run. Use the existing `pass` counter and
the `Sync`/`InvoicePendingLines` flow to locate the loop, and keep the existing
nil/error handling intact while adding the bound.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0e92a39a-450c-4c1e-b335-15c34f5d2160

📥 Commits

Reviewing files that changed from the base of the PR and between dd357c8 and c05acc7.

📒 Files selected for processing (1)
  • openmeter/billing/worker/subscriptionsync/service/sync.go

GAlexIHU
GAlexIHU previously approved these changes Jul 1, 2026
@turip turip force-pushed the feat/collected-invoice-line-limit branch from c05acc7 to 0c21b7e Compare July 1, 2026 14:38
Comment thread openmeter/billing/worker/subscriptionsync/service/sync.go Outdated

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/worker/subscriptionsync/service/sync.go (1)

33-73: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Early return inside the chunk loop can silently skip remaining, still-valid chunks.

lineIDChunks is now a fixed partition of the specific target line IDs computed upstream (via invoiceLineIDsForPendingCollection), not an open-ended poll. When any chunk's call returns ErrInvoiceCreateNoLines or an empty invoices slice, the function returns nil immediately — abandoning any subsequent chunks even though they contain distinct, still-eligible line IDs. This looks like a leftover from the old "loop until exhausted" pattern, which doesn't fit the new bounded-chunk design. Impact is softened since untouched lines stay pending for the next sync pass, but within this pass some eligible lines get dropped without cause.

🐛 Proposed fix: continue to the next chunk instead of aborting
 		for _, lineIDChunk := range lineIDChunks {
 			invoices, err := s.billingService.InvoicePendingLines(
 				ctx,
 				billing.InvoicePendingLinesInput{
 					Customer:            customer,
 					IncludePendingLines: mo.Some(lineIDChunk),
 				},
 				billing.WithPartialInvoiceLinesDisabled(),
 				billing.WithMaxLinesPerInvoice(s.featureFlags.MaxLinesPerCollectedInvoice),
 			)
 			if err != nil {
 				if errors.Is(err, billing.ErrInvoiceCreateNoLines) {
-					return nil
+					continue
 				}
 
 				return err
 			}
 
 			if len(invoices) == 0 {
-				return nil
+				continue
 			}
 		}
🤖 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/worker/subscriptionsync/service/sync.go` around lines 33 -
73, The chunk loop in invoicePendingLines is aborting the whole sync pass too
early when Billing returns ErrInvoiceCreateNoLines or an empty invoices slice,
which can skip later valid chunks. Update the control flow in
invoicePendingLines so that these cases only skip the current lineIDChunk and
continue processing the remaining chunks instead of returning nil from the whole
function. Keep the existing error handling for real failures and preserve the
current chunking behavior driven by s.featureFlags.MaxLinesPerCollectedInvoice.
🧹 Nitpick comments (2)
openmeter/billing/worker/subscriptionsync/service/sync_unit_test.go (1)

70-94: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test gap: no coverage for a mid-batch empty chunk followed by more valid chunks.

This test only uses a single line ID, so lineIDChunks has exactly one entry — it can't actually exercise "exit early vs. continue" behavior since there's no subsequent chunk to skip. Given the loop-termination concern raised in sync.go (lines 33-73), a test with ≥2 chunks where an earlier chunk returns empty/ErrInvoiceCreateNoLines while a later chunk still has real invoices would catch that regression; current tests would pass either way.

As per path instructions: "Suggest missing tests only when the PR introduces behavior without meaningful coverage, or when the current tests would pass despite a concrete regression in the changed code."

🤖 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/worker/subscriptionsync/service/sync_unit_test.go` around
lines 70 - 94, Add a test in invoicePendingLinesExitsOnEmptySuccess that uses
multiple line chunks so it actually exercises the loop in
Service.invoicePendingLines: make invoicePendingLinesBillingService return an
empty success or ErrInvoiceCreateNoLines for an earlier chunk and a real invoice
result for a later chunk. Verify the method continues past the empty chunk
instead of stopping early by asserting both billingService calls are made and
the later chunk is processed correctly.

Source: Path instructions

openmeter/billing/worker/subscriptionsync/service/sync.go (1)

279-366: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a short docstring for the new domain helpers.

invoiceLineIDsForPendingCollection and isLineInvoiceableNow compress non-obvious business rules (dedup across create/update patches, gathering-invoice-only updates, and truncating to streaming.MinimumWindowSizeDuration to decide "invoiceable now"). A brief comment on the observable contract (e.g., why truncation to the minimum window is used to decide eligibility) would help future readers avoid re-deriving the intent from the implementation.

As per coding guidelines: "Add docstrings to domain helpers when the name compresses important business semantics, and describe the observable business contract rather than implementation mechanics."

🤖 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/worker/subscriptionsync/service/sync.go` around lines 279 -
366, Add brief docstrings/comments for the new domain helpers
`invoiceLineIDsForPendingCollection` and `isLineInvoiceableNow`. The issue is
that these helpers encode important business rules that are not obvious from
their names alone, so document the observable contract: which invoice patch
cases are included, that line IDs are deduplicated, and that “invoiceable now”
is determined by comparing `GetInvoiceAt()` against `clock.Now()` after
truncating to `streaming.MinimumWindowSizeDuration`. Keep the comments focused
on intent rather than implementation details.

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.

Outside diff comments:
In `@openmeter/billing/worker/subscriptionsync/service/sync.go`:
- Around line 33-73: The chunk loop in invoicePendingLines is aborting the whole
sync pass too early when Billing returns ErrInvoiceCreateNoLines or an empty
invoices slice, which can skip later valid chunks. Update the control flow in
invoicePendingLines so that these cases only skip the current lineIDChunk and
continue processing the remaining chunks instead of returning nil from the whole
function. Keep the existing error handling for real failures and preserve the
current chunking behavior driven by s.featureFlags.MaxLinesPerCollectedInvoice.

---

Nitpick comments:
In `@openmeter/billing/worker/subscriptionsync/service/sync_unit_test.go`:
- Around line 70-94: Add a test in invoicePendingLinesExitsOnEmptySuccess that
uses multiple line chunks so it actually exercises the loop in
Service.invoicePendingLines: make invoicePendingLinesBillingService return an
empty success or ErrInvoiceCreateNoLines for an earlier chunk and a real invoice
result for a later chunk. Verify the method continues past the empty chunk
instead of stopping early by asserting both billingService calls are made and
the later chunk is processed correctly.

In `@openmeter/billing/worker/subscriptionsync/service/sync.go`:
- Around line 279-366: Add brief docstrings/comments for the new domain helpers
`invoiceLineIDsForPendingCollection` and `isLineInvoiceableNow`. The issue is
that these helpers encode important business rules that are not obvious from
their names alone, so document the observable contract: which invoice patch
cases are included, that line IDs are deduplicated, and that “invoiceable now”
is determined by comparing `GetInvoiceAt()` against `clock.Now()` after
truncating to `streaming.MinimumWindowSizeDuration`. Keep the comments focused
on intent rather than implementation details.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bc452452-cdca-4c9f-9c9b-ee2b651b698f

📥 Commits

Reviewing files that changed from the base of the PR and between c05acc7 and 0c21b7e.

📒 Files selected for processing (2)
  • openmeter/billing/worker/subscriptionsync/service/sync.go
  • openmeter/billing/worker/subscriptionsync/service/sync_unit_test.go

@turip turip force-pushed the feat/collected-invoice-line-limit branch from 0c21b7e to a0597a6 Compare July 1, 2026 14:59
Comment thread openmeter/billing/worker/subscriptionsync/service/sync.go Outdated
Comment thread openmeter/billing/service/gatheringinvoicependinglines.go
@turip turip enabled auto-merge (squash) July 1, 2026 15:13
@turip turip force-pushed the feat/collected-invoice-line-limit branch from 97242a9 to 4560541 Compare July 1, 2026 15:16
tothandras
tothandras previously approved these changes Jul 1, 2026
Comment thread openmeter/billing/worker/subscriptionsync/service/sync.go
@turip turip merged commit 460ee12 into main Jul 1, 2026
26 checks passed
@turip turip deleted the feat/collected-invoice-line-limit branch July 1, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/billing kind/feature New feature or request release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants