Skip to content

feat: move legacy splitline handling to lineengine - #4750

Draft
turip wants to merge 2 commits into
mainfrom
feat/move-legacy-splitline-handling-to-lineengine-reconstructed
Draft

feat: move legacy splitline handling to lineengine#4750
turip wants to merge 2 commits into
mainfrom
feat/move-legacy-splitline-handling-to-lineengine-reconstructed

Conversation

@turip

@turip turip commented Jul 19, 2026

Copy link
Copy Markdown
Member

Summary

  • move legacy split-line-group adapter, service, and domain handling under openmeter/billing/invoicing/legacy/splitlinegroup
  • route the legacy billing line engine through the dedicated split-line-group package
  • begin removing hierarchy embedding from standard and gathering lines and shifting rating and snapshot inputs to the new ownership boundary

Stack

Draft state

This is intentionally a mid-refactor draft. It is known not to build, and no build-error fixes were applied while reconstructing the stack.

Validation

  • not run; compile failures are expected at this intermediate refactor state

Greptile Summary

This is an intentional mid-refactor draft (acknowledged as non-building) that extracts all legacy split-line-group domain types, adapter, and service from the top-level billing package into a dedicated openmeter/billing/invoicing/legacy/splitlinegroup package, and wires the legacy line-engine path through those new boundaries.

  • Package extraction: billing.SplitLineHierarchy, SplitLineGroup, and all related types are deleted from billing/invoicelinesplitgroup.go and re-introduced under splitlinegroup/, where the heterogeneous Lines []LineWithInvoiceHeader slice is replaced with typed StandardLines []StandardLine and GatheringLine *GatheringLine fields backed by the new read-only LineHeaderAccessor interface.
  • Update-patch redesign: PatchLineUpdate switches from carrying a full GenericInvoiceLine snapshot to an intent-based NewUpdateLinePatchInput (only the fields being changed), with an Apply method that merges the diff onto the existing line.
  • Hierarchy fields removed: SplitLineHierarchy is deleted from both billing.StandardLine and billing.GatheringLine; the newly-added PatchLineUpdate.Apply still references billing.GatheringLine.SplitLineHierarchy in the same PR, leaving the gathering-line hierarchy-preservation path without an implementation.

Confidence Score: 3/5

  • This is an explicitly acknowledged mid-refactor draft that does not compile; it should not be merged until the outstanding migration work is complete.
  • Multiple code paths are only partially migrated: AddShrink and AddExtend in patchinvoicelinehierarchy.go still reference old field shapes, updateMutableStandardInvoice no longer snapshots quantities after applying line updates, and the newly introduced PatchLineUpdate.Apply immediately references a billing.GatheringLine.SplitLineHierarchy field that was deleted in the same PR — leaving gathering-line hierarchy preservation without any implementation. The lineengine wiring changes and the splitlinegroup package extraction are structurally sound, but the PR cannot be validated until the compilation failures and the broken Apply logic are resolved.
  • openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go (Apply references removed field), openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go (AddShrink/AddExtend not fully migrated), openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go (quantity snapshotting removed from mutable invoice path)

Important Files Changed

Filename Overview
openmeter/billing/invoicing/legacy/splitlinegroup/invoicelinesplitgroup.go New domain types for split-line groups migrated from billing package. SplitLineHierarchy now holds typed StandardLines/GatheringLine fields replacing the old heterogeneous Lines []LineWithInvoiceHeader. ForEachStandardLine drops the status-based deleted-invoice guard that existed in the old ForEachChild (already flagged).
openmeter/billing/invoicing/legacy/splitlinegroup/headers.go New read-only LineHeaderAccessor interface and associated lightweight StandardLine/GatheringLine/InvoiceHeader header structs. Clean boundary: GetInvoiceID() returns models.NamespacedID, which differs from billing.GenericInvoiceLine.GetInvoiceID() string — callers must use .ID to extract the string (correctly done in AddDelete, not yet fixed in AddShrink).
openmeter/billing/invoicing/legacy/splitlinegroup/service/invoicelinesplitgroup.go Service-layer split-line-group operations. DeleteSplitLineGroup now correctly validates both StandardLines and the optional GatheringLine before deletion — the previously flagged gap appears addressed by this revision.
openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go Replaces generic GenericInvoiceLine target state with intent-based NewUpdateLinePatchInput. The new Apply method references billing.GatheringLine.SplitLineHierarchy which was removed in this same PR, leaving hierarchy-preservation for gathering-line updates without an implementation path.
openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go Partially migrated to LineHeaderAccessor. AddDelete is correctly migrated; AddShrink switches loop source to Lines() but its body still uses child.Line.* (pre-existing flagged issue). AddExtend is entirely unmigrated, still references the removed existingHierarchy.Lines field and billing.LineWithInvoiceHeader type (pre-existing flagged issue).
openmeter/billing/lineengine/engine.go Routes legacy line processing through ResolveSplitLineGroupHeaders and calculateLines before returning from OnCollectionCompleted. LineCalculator interface assertion was dropped; intent is clear but verification depends on calculateLines being present elsewhere.
openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go Switches line-update payload from GenericInvoiceLine to PatchLineUpdate. updateMutableStandardInvoice no longer calls SnapshotLineQuantity after applying the update (pre-existing flagged issue). updateImmutableInvoice retains quantity snapshotting for period-change validation.
openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go Cleanly splits billingService and splitLineGroupService interfaces; normalizePersistedSplitLineHierarchy now mutates StandardLines[i].ServicePeriod and GatheringLine.InvoiceAt directly (value semantics) instead of through interface accessors — correctly reflects the new typed fields.
openmeter/billing/gatheringinvoice.go Removes SplitLineHierarchy field and SetSplitLineHierarchy from billing.GatheringLine; also removes GatheringInvoiceExpandSplitLineHierarchy. Adds GatheringLineWithInvoiceHeader transport type. Removal of the hierarchy field is self-consistent here but creates a dangling reference in the new patch.go.
openmeter/billing/stdinvoiceline.go Removes SplitLineHierarchy field, SetSplitLineHierarchy, GetProgressivelyBilledServicePeriod, GetPreviouslyBilledAmount, IsProgressivelyBilled, and WithoutSplitLineHierarchy from StandardLine. Adds StandardLineWithInvoiceHeader. Responsibility for progressive billing math is moving to the splitlinegroup package.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph OLD["Before: billing package"]
        OA["billing.SplitLineHierarchy"] --> OB["Lines - heterogeneous slice"]
        OD["billing.GatheringLine\n+ SplitLineHierarchy field"] --> OA
        OE["billing.StandardLine\n+ SplitLineHierarchy field"] --> OA
    end

    subgraph NEW["After: splitlinegroup package"]
        NA["splitlinegroup.SplitLineHierarchy"] --> NB["StandardLines typed slice"]
        NA --> NC["GatheringLine pointer"]
        NB --> ND["LineHeaderAccessor\nread-only interface"]
        NC --> ND
        NE["splitlinegroup.Service"] --> NF["splitlinegroup.Adapter"]
    end

    subgraph ENGINE["Line Engine"]
        LE["OnCollectionCompleted"] --> RS["ResolveSplitLineGroupHeaders"]
        RS --> SQ["SnapshotLineQuantities"]
        SQ --> CL["calculateLines"]
    end

    subgraph UPDATER["Subscription Sync Updater"]
        PU["PatchLineUpdate.Apply\nreferences removed field WARN"] --> OD
        UPS["updateMutableStandardInvoice\nno SnapshotLineQuantity WARN"] --> CL
    end

    OLD -.->|"migrated to"| NEW
    NEW --> ENGINE
    NEW --> UPDATER
Loading

Fix all with Greploop

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go:114-133
**`Apply` references `billing.GatheringLine.SplitLineHierarchy` removed in this same PR**

`billing.GatheringLine.SplitLineHierarchy` and `billing.SplitLineHierarchy` (from `invoicelinesplitgroup.go`) were both deleted in this PR, but the newly-added `PatchLineUpdate.Apply` still references them at lines 120–132. This causes an immediate compile failure in code that was just introduced, independent of the other known draft-state errors. More importantly, the intent — preserving the split-line hierarchy reference across a clone/update of a gathering line — has no replacement mechanism now that the field is gone. Any caller that updates a gathering line via `Apply` will silently lose the hierarchy reference unless the method is updated to carry it through the new `splitlinegroup` package types.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (3): Last reviewed commit: "fix: self-review" | 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 Plus

Run ID: 2394cc0b-bad9-426a-b2a0-453451a5d57e

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/move-legacy-splitline-handling-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 344 to +348

func (s *stubRatingService) GenerateProgressiveBilledDetailedLines(in billingrating.ProgressiveBilledLineAccessor, opts ...billingrating.GenerateDetailedLinesOption) (billingrating.GenerateDetailedLinesResult, error) {
return billingrating.GenerateDetailedLinesResult{}, fmt.Errorf("charges rating must always use the non-progressive billed rating engine")
}

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.

P1 Stub GenerateDetailedLines now returns an empty result instead of s.result

Changing return s.result, nil to return billingrating.GenerateDetailedLinesResult{}, nil means test assertions that previously inspected the returned detailed lines from the charges path will now see an empty result. If any test in this package verifies output content from the stub (e.g., total amounts or generated line shapes), those assertions pass trivially with zero values rather than catching regressions. Double-check whether any test case in engine_test.go reads the result of GenerateDetailedLines calls.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/charges/usagebased/service/rating/delta/engine_test.go
Line: 344-348

Comment:
**Stub `GenerateDetailedLines` now returns an empty result instead of `s.result`**

Changing `return s.result, nil` to `return billingrating.GenerateDetailedLinesResult{}, nil` means test assertions that previously inspected the returned detailed lines from the charges path will now see an empty result. If any test in this package verifies output content from the stub (e.g., total amounts or generated line shapes), those assertions pass trivially with zero values rather than catching regressions. Double-check whether any test case in `engine_test.go` reads the result of `GenerateDetailedLines` calls.

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

Fix in Claude Code Fix in Codex

Comment on lines +200 to +240
func (h SplitLineHierarchy) Lines() []LineHeaderAccessor {
lines := lo.Map(h.StandardLines, func(line StandardLine, _ int) LineHeaderAccessor {
return line
})

if h.GatheringLine != nil {
lines = append(lines, *h.GatheringLine)
}
return lines
}

type SumNetAmountInput struct {
PeriodEndLTE time.Time
IncludeCharges bool
}

// SumNetAmount returns the sum of the net amount (pre-tax) of the progressive billed line and its children
// containing the values for all lines whose period's end is <= in.UpTo and are not deleted or not part of
// an invoice that has been deleted.
// As gathering lines do not represent any kind of actual charge, they are not included in the sum.
func (h *SplitLineHierarchy) SumNetAmount(in SumNetAmountInput) (alpacadecimal.Decimal, error) {
netAmount := alpacadecimal.Zero

err := h.ForEachStandardLine(ForEachStandardLineInput{
PeriodEndLTE: in.PeriodEndLTE,
Callback: func(line StandardLine) error {
netAmount = netAmount.Add(line.Totals.Amount)

if in.IncludeCharges {
netAmount = netAmount.Add(line.Totals.ChargesTotal)
}

return nil
},
})
if err != nil {
return alpacadecimal.Zero, err
}

return netAmount, nil
}

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 ForEachStandardLine drops the deleted-invoice status check present in the old ForEachChild

The previous ForEachChild explicitly skipped lines from invoices with Status == StandardInvoiceStatusDeleted. The new ForEachStandardLine only checks line.Invoice.DeletedAt != nil. InvoiceHeader has no Status field, so that check is gone. If an invoice transitions to a deleted status without setting DeletedAt, lines from logically deleted invoices will be included in SumNetAmount, causing progressive billing to under-charge by counting already-deleted prior-period amounts.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/invoicing/legacy/splitlinegroup/invoicelinesplitgroup.go
Line: 200-240

Comment:
**`ForEachStandardLine` drops the deleted-invoice status check present in the old `ForEachChild`**

The previous `ForEachChild` explicitly skipped lines from invoices with `Status == StandardInvoiceStatusDeleted`. The new `ForEachStandardLine` only checks `line.Invoice.DeletedAt != nil`. `InvoiceHeader` has no `Status` field, so that check is gone. If an invoice transitions to a deleted status without setting `DeletedAt`, lines from logically deleted invoices will be included in `SumNetAmount`, causing progressive billing to under-charge by counting already-deleted prior-period amounts.

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

Fix in Claude Code Fix in Codex

@turip
turip force-pushed the feat/move-quantity-snapshot-to-lineengine-reconstructed branch 2 times, most recently from 0b740b7 to e105b4c Compare August 11, 2026 14:00
Base automatically changed from feat/move-quantity-snapshot-to-lineengine-reconstructed to main August 12, 2026 07:55
@turip
turip force-pushed the feat/move-legacy-splitline-handling-to-lineengine-reconstructed branch from 8b49204 to 09f5e35 Compare August 14, 2026 10:51
@turip
turip force-pushed the feat/move-legacy-splitline-handling-to-lineengine-reconstructed branch from 09f5e35 to 5423012 Compare August 14, 2026 13:19
Comment on lines +114 to +133
if line.AsInvoiceLine().Type() == billing.InvoiceLineTypeGathering {
existingGatheringLine, err := line.AsInvoiceLine().AsGatheringLine()
if err != nil {
return nil, fmt.Errorf("converting existing line to gathering line: %w", err)
}

if existingGatheringLine.SplitLineHierarchy != nil {
hierarchy, err := existingGatheringLine.SplitLineHierarchy.Clone()
if err != nil {
return nil, fmt.Errorf("cloning split line hierarchy: %w", err)
}

updatedGatheringLine, err := updatedLine.AsInvoiceLine().AsGatheringLine()
if err != nil {
return nil, fmt.Errorf("converting updated line to gathering line: %w", err)
}

updatedGatheringLine.SplitLineHierarchy = &hierarchy
updatedLine = updatedGatheringLine.AsGenericLine()
}

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.

P1 Apply references billing.GatheringLine.SplitLineHierarchy removed in this same PR

billing.GatheringLine.SplitLineHierarchy and billing.SplitLineHierarchy (from invoicelinesplitgroup.go) were both deleted in this PR, but the newly-added PatchLineUpdate.Apply still references them at lines 120–132. This causes an immediate compile failure in code that was just introduced, independent of the other known draft-state errors. More importantly, the intent — preserving the split-line hierarchy reference across a clone/update of a gathering line — has no replacement mechanism now that the field is gone. Any caller that updates a gathering line via Apply will silently lose the hierarchy reference unless the method is updated to carry it through the new splitlinegroup package types.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/patch.go
Line: 114-133

Comment:
**`Apply` references `billing.GatheringLine.SplitLineHierarchy` removed in this same PR**

`billing.GatheringLine.SplitLineHierarchy` and `billing.SplitLineHierarchy` (from `invoicelinesplitgroup.go`) were both deleted in this PR, but the newly-added `PatchLineUpdate.Apply` still references them at lines 120–132. This causes an immediate compile failure in code that was just introduced, independent of the other known draft-state errors. More importantly, the intent — preserving the split-line hierarchy reference across a clone/update of a gathering line — has no replacement mechanism now that the field is gone. Any caller that updates a gathering line via `Apply` will silently lose the hierarchy reference unless the method is updated to carry it through the new `splitlinegroup` package types.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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