Skip to content

refactor: split get*subscription calls#4748

Draft
turip wants to merge 1 commit into
mainfrom
feat/split-get-subscription-calls-reconstructed
Draft

refactor: split get*subscription calls#4748
turip wants to merge 1 commit 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

Greptile Summary

This PR splits the single GetLinesForSubscription call (which returned a LineOrHierarchy union type) into three typed, per-concern methods: GetStandardLinesForSubscription, GetGatheringLinesForSubscription, and GetSplitLineGroupsForSubscription. The LineOrHierarchy wrapper type and its helpers (NewItemFromLineOrHierarchy, GetDeletePatchesForLine) are removed, with assembly of the combined result moved into persistedstate.Loader.

  • The adapter layer gains three focused query functions—each restricted to its own invoice-status filter—replacing the monolithic query that mixed standard, gathering, and split-line results in one transaction.
  • The loader now makes three sequential DB calls and assembles the result locally, including the duplicate-ChildUniqueReferenceID sanity check that previously lived in the adapter.
  • Noop stubs in server_test.go are updated to match the new interface split.

Confidence Score: 4/5

Safe to merge; the refactor is well-scoped, all removed symbols have no remaining callers, and the 6k+ test suite passes. The one thing worth a second look is the loader making three sequential DB calls with independent transactions where a single transaction previously gave a consistent snapshot.

The split is mechanically correct — filter predicates on the three new adapter methods are complementary (StatusNEQ Gathering / StatusEQ Gathering / split-line-group table), removed types have zero remaining references, and the duplicate-ID sanity check is preserved in the loader. The behavioral change is that LoadForSubscription now reads across three separate transactions; a concurrent status transition between reads could produce a torn view and trigger the duplicate-ID guard or silently drop a line for one sync cycle. Given that subscription sync is designed to be idempotent this is unlikely to cause lasting harm, but the transactional guarantee that existed before is no longer present.

openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go — three sequential independent transactions replacing one atomic read.

Important Files Changed

Filename Overview
openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go LoadForSubscription now makes three sequential, independently-transacted DB calls instead of one. Duplicate-ID check, normalization, and item assembly are clean; three-call pattern removes transactional snapshot guarantee.
openmeter/billing/adapter/stdinvoicelines.go Renamed GetLinesForSubscription → GetStandardLinesForSubscription, adds StatusNEQ(Gathering) filter, removes the gathering and split-line-group sub-logic; clean split.
openmeter/billing/adapter/gatheringlines.go New GetGatheringLinesForSubscription implementation; mirrors the old gathering-lines path with StatusEQ(Gathering) filter, correct schema-level lookup, and invoice nil-guard.
openmeter/billing/adapter/invoicelinesplitgroup.go New GetSplitLineGroupsForSubscription; correctly filters by namespace, subscription ID, and DeletedAtIsNil; maps via existing helpers.
openmeter/billing/invoicelinesplitgroup.go Removes the LineOrHierarchy union type and all its methods; no remaining references confirm this dead code is fully cleaned up.
openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go Removes GetDeletePatchesForLine (depended on LineOrHierarchy); no remaining callers found.
openmeter/billing/service.go Interface declarations correctly updated: GetLinesForSubscription removed, three typed methods added to their respective service interfaces.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Loader as persistedstate.Loader
    participant Svc as billing.Service
    participant AdapterStd as adapter (stdinvoicelines)
    participant AdapterGath as adapter (gatheringlines)
    participant AdapterSplit as adapter (invoicelinesplitgroup)
    participant DB as Database

    Note over Loader,DB: Before (single transaction)
    Loader->>Svc: GetLinesForSubscription(input)
    Svc->>AdapterStd: GetLinesForSubscription(input)
    AdapterStd->>DB: BEGIN TX
    AdapterStd->>DB: BillingInvoiceLine (std+gathering) + SplitLineGroup
    DB-->>AdapterStd: LineOrHierarchy[]
    AdapterStd->>DB: COMMIT
    AdapterStd-->>Svc: []LineOrHierarchy
    Svc-->>Loader: []LineOrHierarchy

    Note over Loader,DB: After (three separate transactions)
    Loader->>Svc: GetStandardLinesForSubscription(input)
    Svc->>AdapterStd: GetStandardLinesForSubscription(input)
    AdapterStd->>DB: BEGIN TX (StatusNEQ Gathering)
    DB-->>AdapterStd: StandardLines
    AdapterStd->>DB: COMMIT
    AdapterStd-->>Svc: StandardLines
    Svc-->>Loader: StandardLines

    Loader->>Svc: GetGatheringLinesForSubscription(input)
    Svc->>AdapterGath: GetGatheringLinesForSubscription(input)
    AdapterGath->>DB: BEGIN TX (StatusEQ Gathering)
    DB-->>AdapterGath: GatheringLines
    AdapterGath->>DB: COMMIT
    AdapterGath-->>Svc: GatheringLines
    Svc-->>Loader: GatheringLines

    Loader->>Svc: GetSplitLineGroupsForSubscription(input)
    Svc->>AdapterSplit: GetSplitLineGroupsForSubscription(input)
    AdapterSplit->>DB: BEGIN TX (SplitLineGroup)
    DB-->>AdapterSplit: []SplitLineHierarchy
    AdapterSplit->>DB: COMMIT
    AdapterSplit-->>Svc: []SplitLineHierarchy
    Svc-->>Loader: []SplitLineHierarchy

    Loader->>Loader: concat + normalize + duplicate-ID check
    Loader->>Loader: "assemble State{byUniqueID, invoices, charges}"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Loader as persistedstate.Loader
    participant Svc as billing.Service
    participant AdapterStd as adapter (stdinvoicelines)
    participant AdapterGath as adapter (gatheringlines)
    participant AdapterSplit as adapter (invoicelinesplitgroup)
    participant DB as Database

    Note over Loader,DB: Before (single transaction)
    Loader->>Svc: GetLinesForSubscription(input)
    Svc->>AdapterStd: GetLinesForSubscription(input)
    AdapterStd->>DB: BEGIN TX
    AdapterStd->>DB: BillingInvoiceLine (std+gathering) + SplitLineGroup
    DB-->>AdapterStd: LineOrHierarchy[]
    AdapterStd->>DB: COMMIT
    AdapterStd-->>Svc: []LineOrHierarchy
    Svc-->>Loader: []LineOrHierarchy

    Note over Loader,DB: After (three separate transactions)
    Loader->>Svc: GetStandardLinesForSubscription(input)
    Svc->>AdapterStd: GetStandardLinesForSubscription(input)
    AdapterStd->>DB: BEGIN TX (StatusNEQ Gathering)
    DB-->>AdapterStd: StandardLines
    AdapterStd->>DB: COMMIT
    AdapterStd-->>Svc: StandardLines
    Svc-->>Loader: StandardLines

    Loader->>Svc: GetGatheringLinesForSubscription(input)
    Svc->>AdapterGath: GetGatheringLinesForSubscription(input)
    AdapterGath->>DB: BEGIN TX (StatusEQ Gathering)
    DB-->>AdapterGath: GatheringLines
    AdapterGath->>DB: COMMIT
    AdapterGath-->>Svc: GatheringLines
    Svc-->>Loader: GatheringLines

    Loader->>Svc: GetSplitLineGroupsForSubscription(input)
    Svc->>AdapterSplit: GetSplitLineGroupsForSubscription(input)
    AdapterSplit->>DB: BEGIN TX (SplitLineGroup)
    DB-->>AdapterSplit: []SplitLineHierarchy
    AdapterSplit->>DB: COMMIT
    AdapterSplit-->>Svc: []SplitLineHierarchy
    Svc-->>Loader: []SplitLineHierarchy

    Loader->>Loader: concat + normalize + duplicate-ID check
    Loader->>Loader: "assemble State{byUniqueID, invoices, charges}"
Loading

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go:52-65
**Three separate DB transactions replace one**

`LoadForSubscription` now issues three independent transactions where the original `GetLinesForSubscription` ran a single transaction that fetched standard lines, gathering lines, and split-line groups atomically. If a line transitions between statuses (e.g., gathering → standard invoice advancement) or a new split-line group is written between the three reads, the loader could observe a torn snapshot: the same line might appear in both the standard and gathering result sets, or the duplicate-`ChildUniqueReferenceID` guard could fail spuriously. Because subscription sync is idempotent and typically the sole writer for these entities, this window is small—but the snapshot guarantee is gone. Wrapping the three calls in a single outer transaction (or a read-only snapshot) at the service or loader level would restore the previous atomicity without changing the cleaner per-method API.

Reviews (1): Last reviewed commit: "refactor: split get*subscription calls" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5ecfee1a-35ad-45c9-862a-62ea3b39141e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 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.

Comment on lines +52 to +65
standardLines, err := l.billingService.GetStandardLinesForSubscription(ctx, getLinesInput)
if err != nil {
return State{}, fmt.Errorf("getting existing standard lines: %w", err)
}

gatheringLines, err := l.billingService.GetGatheringLinesForSubscription(ctx, getLinesInput)
if err != nil {
return State{}, fmt.Errorf("getting existing lines: %w", err)
return State{}, fmt.Errorf("getting existing gathering lines: %w", err)
}

lines, err = slicesx.MapWithErr(lines, normalizePersistedLineOrHierarchy)
splitLineGroups, err := l.billingService.GetSplitLineGroupsForSubscription(ctx, getLinesInput)
if err != nil {
return State{}, fmt.Errorf("getting existing split line groups: %w", err)
}

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 Three separate DB transactions replace one

LoadForSubscription now issues three independent transactions where the original GetLinesForSubscription ran a single transaction that fetched standard lines, gathering lines, and split-line groups atomically. If a line transitions between statuses (e.g., gathering → standard invoice advancement) or a new split-line group is written between the three reads, the loader could observe a torn snapshot: the same line might appear in both the standard and gathering result sets, or the duplicate-ChildUniqueReferenceID guard could fail spuriously. Because subscription sync is idempotent and typically the sole writer for these entities, this window is small—but the snapshot guarantee is gone. Wrapping the three calls in a single outer transaction (or a read-only snapshot) at the service or loader level would restore the previous atomicity without changing the cleaner per-method API.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go
Line: 52-65

Comment:
**Three separate DB transactions replace one**

`LoadForSubscription` now issues three independent transactions where the original `GetLinesForSubscription` ran a single transaction that fetched standard lines, gathering lines, and split-line groups atomically. If a line transitions between statuses (e.g., gathering → standard invoice advancement) or a new split-line group is written between the three reads, the loader could observe a torn snapshot: the same line might appear in both the standard and gathering result sets, or the duplicate-`ChildUniqueReferenceID` guard could fail spuriously. Because subscription sync is idempotent and typically the sole writer for these entities, this window is small—but the snapshot guarantee is gone. Wrapping the three calls in a single outer transaction (or a read-only snapshot) at the service or loader level would restore the previous atomicity without changing the cleaner per-method API.

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

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant