Skip to content

refactor: split get*subscription calls - #4748

Merged
turip merged 2 commits into
mainfrom
feat/split-get-subscription-calls-reconstructed
Aug 11, 2026
Merged

refactor: split get*subscription calls#4748
turip merged 2 commits into
mainfrom
feat/split-get-subscription-calls-reconstructed

Conversation

@turip

@turip turip commented Jul 19, 2026

Copy link
Copy Markdown
Member

Summary

  • split the combined subscription line read into typed standard-line, gathering-line, and split-line-group methods
  • move LineOrHierarchy ownership into subscription sync and assemble typed results there
  • keep split-line-group loading separate from standard and gathering invoice lines

Motivation

This separates the subscription read paths as a prerequisite for moving split-line-group handling into the legacy billing line engine.

Reconstruction note

This draft reconstructs the original commit from #4745. The existing #4745 branch and PR were intentionally left unchanged.

Validation

  • make test-nocache: 6,156 passed, 9 skipped on the original commit

Summary by CodeRabbit

  • New Features
    • Subscription synchronization now separately retrieves standard invoice lines, gathering lines, and split-line groups.
    • Split-line groups and their related invoice details are preserved during synchronization.
  • Improvements
    • Billing data is categorized more clearly, improving consistency when loading and reconciling subscription records.
    • Duplicate records and invalid subscription data are detected more reliably.
    • Invoice updates and persisted subscription state now handle each billing line type independently.

Greptile Summary

This PR refactors the subscription billing read path by splitting the single combined GetLinesForSubscription method (returning the LineOrHierarchy union type) into three typed methods — GetStandardLinesForSubscription, GetGatheringLinesForSubscription, and GetSplitLineGroupsForSubscription — each expressed through their respective service and adapter interfaces. The LineOrHierarchy union type, its constructors, and the now-unused GetDeletePatchesForLine helper are removed.

  • Adapter-level filtering by invoice status is pushed to SQL (HasBillingInvoiceWith) rather than done in-memory, keeping the query results inherently typed.
  • Assembly of typed Items and duplicate ChildUniqueReferenceID detection are moved from the adapter layer to the loader, using lo.GroupBy+MapValuesErr with richer error messages than before.
  • Normalization logic (normalizePersistedLine, normalizePersistedSplitLineHierarchy) is cleanly separated into two focused functions.

Confidence Score: 5/5

  • This PR is a clean structural refactor with no functional changes; all existing behavior is preserved and tests fully pass.
  • The change is a pure API split across well-tested, narrowly scoped layers. The duplicate unique-ID guard is preserved (and improved with type info in the error message), the SQL filters are semantically equivalent to the prior in-memory splits, and every deleted helper (LineOrHierarchy, NewItemFromLineOrHierarchy, GetDeletePatchesForLine) is confirmed unused in the rest of the codebase. No logic is altered, only decomposed.
  • No files require special attention.

Important Files Changed

Filename Overview
openmeter/billing/adapter.go Moves GetLinesForSubscription off InvoiceLineAdapter; adds typed GetStandardLinesForSubscription, GetGatheringLinesForSubscription, and GetSplitLineGroupsForSubscription to their respective adapter interfaces. Clean interface split with no functional change.
openmeter/billing/adapter/stdinvoicelines.go Renames GetLinesForSubscription to GetStandardLinesForSubscription, adds SQL-level HasBillingInvoiceWith(StatusNEQ(Gathering)) filter (previously done in-memory), and strips the now-separate gathering/split-group handling. Logically equivalent to the prior in-memory filter.
openmeter/billing/adapter/gatheringlines.go New GetGatheringLinesForSubscription method extracted from the old combined adapter. Queries lines filtered to gathering invoices, groups by invoice for schema-level mapping, and returns typed GatheringLines. Pattern mirrors the standard-line adapter.
openmeter/billing/adapter/invoicelinesplitgroup.go New GetSplitLineGroupsForSubscription method extracted from the old combined adapter. Uses the existing mapSplitLineHierarchyFromDB helper, which is now also used in UpdateSplitLineGroup and GetSplitLineGroup, ensuring consistent mapping logic.
openmeter/billing/invoicelinesplitgroup.go Removes the LineOrHierarchy union type, its constructors, and all accessor methods. This type's role is now replaced by the typed StandardLines/GatheringLines/SplitLineHierarchy results assembled directly in the loader.
openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go LoadForSubscription now calls three typed methods and assembles Items from typed inputs instead of wrapping LineOrHierarchy. Duplicate-ID detection is preserved via lo.GroupBy+MapValuesErr with richer error messages. normalizePersistedLine and normalizePersistedSplitLineHierarchy are cleanly separated.
openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go Removes NewItemFromLineOrHierarchy which is no longer needed; direct typed construction via newPersistedLine and newPersistedSplitLineHierarchy is now called from the loader. No functional change to Item types.
openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go Removes GetDeletePatchesForLine which was the only consumer of LineOrHierarchy in this package. The function is no longer referenced anywhere in the repo.
openmeter/billing/service.go Removes GetLinesForSubscription from InvoiceLineService and adds the three typed methods to their respective service interfaces (StandardInvoiceService, GatheringInvoiceService, SplitLineGroupService) with clear doc comments.
openmeter/server/server_test.go Updates NoopBillingService to implement the three new interface methods and drops the old GetLinesForSubscription stub. Straightforward test adapter update.

Sequence Diagram

sequenceDiagram
    participant Loader
    participant BillingService
    participant Adapter
    participant DB

    Note over Loader: LoadForSubscription()
    Loader->>BillingService: GetStandardLinesForSubscription(input)
    BillingService->>Adapter: GetStandardLinesForSubscription(input)
    Adapter->>DB: "BillingInvoiceLine WHERE status != Gathering"
    DB-->>Adapter: []StandardLine rows
    Adapter-->>BillingService: StandardLines
    BillingService-->>Loader: StandardLines

    Loader->>BillingService: GetGatheringLinesForSubscription(input)
    BillingService->>Adapter: GetGatheringLinesForSubscription(input)
    Adapter->>DB: "BillingInvoiceLine WHERE status = Gathering"
    DB-->>Adapter: []GatheringLine rows
    Adapter-->>BillingService: GatheringLines
    BillingService-->>Loader: GatheringLines

    Loader->>BillingService: GetSplitLineGroupsForSubscription(input)
    BillingService->>Adapter: GetSplitLineGroupsForSubscription(input)
    Adapter->>DB: BillingInvoiceSplitLineGroup WITH lines+invoices
    DB-->>Adapter: []SplitLineHierarchy rows
    Adapter-->>BillingService: []SplitLineHierarchy
    BillingService-->>Loader: []SplitLineHierarchy

    Note over Loader: Assemble typed Items
    Note over Loader: Normalize timestamps (per type)
    Note over Loader: Duplicate uniqueID detection (lo.GroupBy)
    Note over Loader: Load invoices for items
Loading

Reviews (3): Last reviewed commit: "refactor: reuse split line hierarchy map..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The billing API now retrieves standard lines, gathering lines, and split-line groups through separate typed methods. Adapters implement type-specific queries. Subscription synchronization loads, normalizes, combines, and validates these collections independently.

Changes

Subscription line retrieval

Layer / File(s) Summary
Typed retrieval contracts
openmeter/billing/adapter.go, openmeter/billing/service.go, openmeter/billing/service/..., openmeter/server/server_test.go
The generic subscription retrieval method is replaced by separate standard-line, gathering-line, and split-line-group methods.
Typed adapter retrieval
openmeter/billing/adapter/stdinvoicelines.go, openmeter/billing/adapter/gatheringlines.go, openmeter/billing/adapter/invoicelinesplitgroup.go
Adapters apply type-specific filters, validation, invoice schema loading, grouping, and mapping.
Subscription sync loading and normalization
openmeter/billing/worker/subscriptionsync/service/persistedstate/...
The loader retrieves each line type separately, normalizes values, combines persisted items, detects duplicate identifiers, and extracts invoice identifiers.
Legacy line union cleanup
openmeter/billing/invoicelinesplitgroup.go, openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go, openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go
The LineOrHierarchy abstraction and its legacy conversion and deletion-patch helpers are removed.

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

Possibly related PRs

Suggested reviewers: 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 summarizes the main refactor by splitting the subscription retrieval calls into separate methods.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/split-get-subscription-calls-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.

@turip
turip force-pushed the feat/split-get-subscription-calls-reconstructed branch from 97279e3 to 4ef880a Compare August 11, 2026 12:18
@turip
turip marked this pull request as ready for review August 11, 2026 12:19
@turip
turip requested a review from a team as a code owner August 11, 2026 12:19
@turip turip added release-note/misc Miscellaneous changes area/billing labels Aug 11, 2026

@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 (2)
openmeter/billing/adapter/gatheringlines.go (1)

38-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider sharing the query predicates with GetStandardLinesForSubscription.

This predicate chain is identical to openmeter/billing/adapter/stdinvoicelines.go lines 805-827. Only the invoice-status predicate differs: StatusEQ here, StatusNEQ there. The two filters must stay exactly complementary, otherwise subscription sync silently loses or duplicates lines.

A small shared builder that takes the status predicate would keep that invariant in one place. Fully optional for this PR.

♻️ Sketch of a shared builder
func (a *adapter) subscriptionLinesQuery(
	q *db.BillingInvoiceLineQuery,
	in billing.GetLinesForSubscriptionInput,
	invoiceStatus predicate.BillingInvoice,
) *db.BillingInvoiceLineQuery {
	q = q.
		Where(billinginvoiceline.Namespace(in.Namespace)).
		Where(billinginvoiceline.SubscriptionID(in.SubscriptionID)).
		// Split-line children are loaded through their hierarchy instead of as independent subscription items.
		Where(billinginvoiceline.ParentLineIDIsNil()).
		Where(billinginvoiceline.HasBillingInvoiceWith(invoiceStatus)).
		Where(billinginvoiceline.Or(
			billinginvoiceline.DeletedAtIsNil(),
			billinginvoiceline.And(
				billinginvoiceline.DeletedAtNotNil(),
				billinginvoiceline.ManagedByEQ(billing.ManuallyManagedLine),
			),
		)).
		WithBillingInvoice(func(q *db.BillingInvoiceQuery) {
			q.Where(billinginvoice.Namespace(in.Namespace))
		})

	if !in.IncludeChargeManaged {
		q = q.Where(billinginvoiceline.ChargeIDIsNil())
	}

	return a.expandLineItems(q, in.Namespace)
}
🤖 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/adapter/gatheringlines.go` around lines 38 - 60, Extract
the shared subscription line predicates from the current query and
GetStandardLinesForSubscription into a common builder, such as
subscriptionLinesQuery, accepting the invoice-status predicate as its variable
input. Update both callers to use it with StatusEQ and StatusNEQ respectively,
while preserving the existing charge-managed filtering and line-item expansion
behavior.
openmeter/billing/adapter/invoicelinesplitgroup.go (1)

52-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the existing mapSplitLineHierarchyFromDB helper.

This closure repeats mapSplitLineHierarchyFromDB (lines 252-269) exactly: same group mapping, same line mapping, same struct assembly. Calling the helper keeps one mapping path, so future changes to hierarchy mapping land in both readers.

♻️ Proposed refactor
-		groups, err := slicesx.MapWithErr(dbGroups, func(dbGroup *db.BillingInvoiceSplitLineGroup) (billing.SplitLineHierarchy, error) {
-			group, err := tx.mapSplitLineGroupFromDB(dbGroup)
-			if err != nil {
-				return billing.SplitLineHierarchy{}, err
-			}
-
-			lines, err := tx.mapSplitLineHierarchyLinesFromDB(ctx, dbGroup.Edges.BillingInvoiceLines)
-			if err != nil {
-				return billing.SplitLineHierarchy{}, err
-			}
-
-			return billing.SplitLineHierarchy{
-				Group: group,
-				Lines: lines,
-			}, nil
-		})
+		groups, err := slicesx.MapWithErr(dbGroups, func(dbGroup *db.BillingInvoiceSplitLineGroup) (billing.SplitLineHierarchy, error) {
+			return tx.mapSplitLineHierarchyFromDB(ctx, dbGroup)
+		})
 		if err != nil {
 			return nil, fmt.Errorf("mapping split line groups: %w", err)
 		}
🤖 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/adapter/invoicelinesplitgroup.go` around lines 52 - 70,
Replace the duplicated mapping closure passed to slicesx.MapWithErr with the
existing mapSplitLineHierarchyFromDB helper. Preserve the current error
propagation and “mapping split line groups” wrapping while routing each dbGroup
through that helper.
🤖 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/adapter/gatheringlines.go`:
- Around line 38-60: Extract the shared subscription line predicates from the
current query and GetStandardLinesForSubscription into a common builder, such as
subscriptionLinesQuery, accepting the invoice-status predicate as its variable
input. Update both callers to use it with StatusEQ and StatusNEQ respectively,
while preserving the existing charge-managed filtering and line-item expansion
behavior.

In `@openmeter/billing/adapter/invoicelinesplitgroup.go`:
- Around line 52-70: Replace the duplicated mapping closure passed to
slicesx.MapWithErr with the existing mapSplitLineHierarchyFromDB helper.
Preserve the current error propagation and “mapping split line groups” wrapping
while routing each dbGroup through that helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a6bb210e-4754-4368-a952-3d8621a9109f

📥 Commits

Reviewing files that changed from the base of the PR and between caeeb30 and 4ef880a.

📒 Files selected for processing (13)
  • openmeter/billing/adapter.go
  • openmeter/billing/adapter/gatheringlines.go
  • openmeter/billing/adapter/invoicelinesplitgroup.go
  • openmeter/billing/adapter/stdinvoicelines.go
  • openmeter/billing/invoicelinesplitgroup.go
  • openmeter/billing/service.go
  • openmeter/billing/service/gatheringinvoiceline.go
  • openmeter/billing/service/invoicelinesplitgroup.go
  • openmeter/billing/service/stdinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go
  • openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go
  • openmeter/server/server_test.go
💤 Files with no reviewable changes (3)
  • openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go
  • openmeter/billing/invoicelinesplitgroup.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go

@turip

turip commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Regarding CodeRabbit’s suggestion to consolidate the standard and gathering subscription-line predicates: I’m intentionally keeping them separate. These loaders are expected to move into separate modules, so extracting a shared query builder here would reintroduce coupling and create temporary churn. The current predicates are deliberately complementary.

@turip
turip enabled auto-merge (squash) August 11, 2026 12:40
@turip
turip merged commit 09ba1fc into main Aug 11, 2026
27 checks passed
@turip
turip deleted the feat/split-get-subscription-calls-reconstructed branch August 11, 2026 13:30
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