Skip to content

feat: move legacy splitline handling to lineengine#4750

Draft
turip wants to merge 1 commit into
feat/move-quantity-snapshot-to-lineengine-reconstructedfrom
feat/move-legacy-splitline-handling-to-lineengine-reconstructed
Draft

feat: move legacy splitline handling to lineengine#4750
turip wants to merge 1 commit into
feat/move-quantity-snapshot-to-lineengine-reconstructedfrom
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 PR moves the legacy split-line-group domain types, adapter interface, and service interface out of the top-level billing package into a dedicated openmeter/billing/invoicing/legacy/splitlinegroup package, and rewires the line engine to carry split-line hierarchy as an explicit wrapper (StandardLineWithSplitLineHierarchy) rather than embedding it in StandardLine.

  • SplitLineHierarchy is redesigned with typed StandardLines []StandardLine and GatheringLine *GatheringLine fields (replacing the generic LinesWithInvoiceHeaders), and a new LineHeaderAccessor interface unifies read-only access across both line types.
  • The rating service gains a GenerateProgressiveBilledDetailedLines method for the legacy line-engine path, while GenerateDetailedLines is guarded against receiving a progressive accessor to prevent accidental overcharging.
  • ResolveSplitLineGroupHeaders is refactored to return []StandardLineWithSplitLineHierarchy instead of mutating lines in place, and Loader/Updater gain an explicit SplitLineGroupService dependency separated from BillingService.

Confidence Score: 3/5

The PR is an intentionally non-building mid-refactor draft; AddShrink and AddExtend in patchinvoicelinehierarchy.go were not migrated to the new interface, and two test helpers have logic gaps that could mask real regressions once the build is restored.

AddShrink and AddExtend still use the old billing.LineWithInvoiceHeader field accessors on what is now a LineHeaderAccessor interface — these methods need to be redesigned, not just re-spelled. The test helper RunCalculationTestCase silently discards tc.UnitConfig after removing unitConfigLineAccessor. The charges-path stub in engine_test.go returns an empty result instead of s.result, which can hide assertion failures. The removal of SnapshotLineQuantity from updateMutableStandardInvoice lacks an explanation of where re-snapshotting now occurs in the mutable invoice update path.

patchinvoicelinehierarchy.go (AddShrink and AddExtend methods), rating/service/testutil/ubptest.go (UnitConfig handling), charges/usagebased/service/rating/delta/engine_test.go (stub result change), invoiceupdater/invoiceupdate.go (SnapshotLineQuantity removal)

Important Files Changed

Filename Overview
openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go AddShrink and AddExtend not migrated to new LineHeaderAccessor interface — still uses old child.Line.* and child.Invoice.* field access patterns that no longer compile.
openmeter/billing/invoicing/legacy/splitlinegroup/invoicelinesplitgroup.go New home for SplitLineGroup domain types moved from billing package; SplitLineHierarchy now carries typed StandardLines/GatheringLine instead of generic LinesWithInvoiceHeaders; ForEachStandardLine drops invoice-status check.
openmeter/billing/lineengine/stdline.go New StandardLineWithSplitLineHierarchy wrapper type carries SplitLineHierarchy out of StandardLine struct; progressive billing helpers moved here from billing.StandardLine.
openmeter/billing/lineengine/stdinvoice.go BuildStandardInvoiceLines now fully inlines calculateLines inside buildStandardInvoiceLinesWithQuantitySnapshot; public CalculateLines removed and calculateLines made private with new typed input.
openmeter/billing/lineengine/splitlinegroup.go ResolveSplitLineGroupHeaders migrated to return []StandardLineWithSplitLineHierarchy instead of mutating lines in-place; SplitLineGroupAdapter types updated to reference new splitlinegroup package.
openmeter/billing/rating/service/detailedline.go Split GenerateDetailedLines into GenerateProgressiveBilledDetailedLines (for legacy line engine) and GenerateDetailedLines (non-progressive, guarded against progressive accessor via type assertion).
openmeter/billing/rating/service/testutil/ubptest.go unitConfigLineAccessor removed and RunCalculationTestCase switched to GenerateProgressiveBilledDetailedLines, but CalculationTestCase.UnitConfig is now silently unused.
openmeter/billing/charges/usagebased/service/rating/delta/engine_test.go GenerateProgressiveBilledDetailedLines stub added; GenerateDetailedLines stub changed to return empty result instead of s.result, potentially masking test assertions.
openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go SplitLineGroupService extracted from BillingService; SnapshotLineQuantity call removed from updateMutableStandardInvoice without a stated replacement.
openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go Loader gains explicit splitLineGroupService dependency; normalizePersistedSplitLineHierarchy updated to iterate typed StandardLines and GatheringLine fields directly.
openmeter/billing/invoicelinesplitgroup.go Deleted — all SplitLineGroup domain types, SplitLineHierarchy, LineWithInvoiceHeader etc. moved to splitlinegroup package or billing sub-files.
openmeter/billing/invoicing/legacy/splitlinegroup/headers.go New file defining typed StandardLine/GatheringLine header structs and LineHeaderAccessor interface for the splitlinegroup package.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph lineengine["lineengine package"]
        E[Engine]
        SLG[splitlinegroup.go\nResolveSplitLineGroupHeaders]
        SI[stdinvoice.go\nbuildStandardInvoiceLinesWithQuantitySnapshot]
        QS[quantitysnapshot.go\nSnapshotLineQuantities]
        SL[stdline.go\nStandardLineWithSplitLineHierarchy]
    end

    subgraph splitlinegroup["invoicing/legacy/splitlinegroup"]
        SLH[SplitLineHierarchy\nStandardLines + GatheringLine]
        LHA[LineHeaderAccessor interface]
        SLGrp[SplitLineGroup]
        Svc[Service interface]
    end

    subgraph rating["rating/service"]
        DL[detailedline.go]
        GPBDL[GenerateProgressiveBilledDetailedLines]
        GDL[GenerateDetailedLines\nnon-progressive only]
    end

    subgraph reconciler["subscriptionsync/reconciler"]
        IU[invoiceupdater]
        PH[patchinvoicelinehierarchy\nAddDelete ✓\nAddShrink ⚠\nAddExtend ⚠]
        Loader[persistedstate/Loader]
    end

    E --> SLG
    SLG --> SL
    SL --> SLH
    SI --> SLG
    SI --> QS
    SI --> DL
    DL --> GPBDL
    DL --> GDL
    SLH --> LHA
    SLH --> SLGrp
    Loader -->|splitLineGroupService| Svc
    IU -->|splitLineGroupService| Svc
    PH --> SLH
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"}}}%%
flowchart TD
    subgraph lineengine["lineengine package"]
        E[Engine]
        SLG[splitlinegroup.go\nResolveSplitLineGroupHeaders]
        SI[stdinvoice.go\nbuildStandardInvoiceLinesWithQuantitySnapshot]
        QS[quantitysnapshot.go\nSnapshotLineQuantities]
        SL[stdline.go\nStandardLineWithSplitLineHierarchy]
    end

    subgraph splitlinegroup["invoicing/legacy/splitlinegroup"]
        SLH[SplitLineHierarchy\nStandardLines + GatheringLine]
        LHA[LineHeaderAccessor interface]
        SLGrp[SplitLineGroup]
        Svc[Service interface]
    end

    subgraph rating["rating/service"]
        DL[detailedline.go]
        GPBDL[GenerateProgressiveBilledDetailedLines]
        GDL[GenerateDetailedLines\nnon-progressive only]
    end

    subgraph reconciler["subscriptionsync/reconciler"]
        IU[invoiceupdater]
        PH[patchinvoicelinehierarchy\nAddDelete ✓\nAddShrink ⚠\nAddExtend ⚠]
        Loader[persistedstate/Loader]
    end

    E --> SLG
    SLG --> SL
    SL --> SLH
    SI --> SLG
    SI --> QS
    SI --> DL
    DL --> GPBDL
    DL --> GDL
    SLH --> LHA
    SLH --> SLGrp
    Loader -->|splitLineGroupService| Svc
    IU -->|splitLineGroupService| Svc
    PH --> SLH
Loading

Comments Outside Diff (4)

  1. openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go, line 94-133 (link)

    P1 Incomplete migration in AddShrink — still uses old field accessors

    AddShrink was only partially migrated. After the call to existingHierarchy.Lines() (which now returns []LineHeaderAccessor), the loop body still accesses child.Line.* and child.Invoice.* — fields that no longer exist on the LineHeaderAccessor interface. Critically, the method also calls child.Line.CloneWithoutChildren(), child.Line.UpdateServicePeriod(), and casts child.Invoice to check invoice type, all of which require the concrete billing.LineWithInvoiceHeader type. Beyond the compile failure, the LineHeaderAccessor interface is intentionally read-only header data, so the mutation logic (SetDeletedAt, UpdateServicePeriod, InvoiceAtAccessor) needs to be re-thought for this path rather than just swapping field access.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go
    Line: 94-133
    
    Comment:
    **Incomplete migration in `AddShrink` — still uses old field accessors**
    
    `AddShrink` was only partially migrated. After the call to `existingHierarchy.Lines()` (which now returns `[]LineHeaderAccessor`), the loop body still accesses `child.Line.*` and `child.Invoice.*` — fields that no longer exist on the `LineHeaderAccessor` interface. Critically, the method also calls `child.Line.CloneWithoutChildren()`, `child.Line.UpdateServicePeriod()`, and casts `child.Invoice` to check invoice type, all of which require the concrete `billing.LineWithInvoiceHeader` type. Beyond the compile failure, the `LineHeaderAccessor` interface is intentionally read-only header data, so the mutation logic (`SetDeletedAt`, `UpdateServicePeriod`, `InvoiceAtAccessor`) needs to be re-thought for this path rather than just swapping field access.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

  2. openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go, line 164-195 (link)

    P1 AddExtend not migrated — still uses old Lines slice type

    AddExtend still accesses existingHierarchy.Lines directly as the old billing.LinesWithInvoiceHeaders slice (type []billing.LineWithInvoiceHeader), uses billing.LineWithInvoiceHeader in the slices.SortFunc comparator, and calls lines[…].Line.CloneWithoutChildren() and lines[…].Invoice.AsInvoice().Type(). None of these exist on the new typed fields (StandardLines []StandardLine, GatheringLine *GatheringLine). Unlike AddDelete and AddShrink which were at least partially migrated to group.Lines(), this method was not touched at all and needs equivalent migration plus logic to handle the typed StandardLines/GatheringLine split.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go
    Line: 164-195
    
    Comment:
    **`AddExtend` not migrated — still uses old `Lines` slice type**
    
    `AddExtend` still accesses `existingHierarchy.Lines` directly as the old `billing.LinesWithInvoiceHeaders` slice (type `[]billing.LineWithInvoiceHeader`), uses `billing.LineWithInvoiceHeader` in the `slices.SortFunc` comparator, and calls `lines[…].Line.CloneWithoutChildren()` and `lines[…].Invoice.AsInvoice().Type()`. None of these exist on the new typed fields (`StandardLines []StandardLine`, `GatheringLine *GatheringLine`). Unlike `AddDelete` and `AddShrink` which were at least partially migrated to `group.Lines()`, this method was not touched at all and needs equivalent migration plus logic to handle the typed `StandardLines`/`GatheringLine` split.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

  3. openmeter/billing/rating/service/testutil/ubptest.go, line 56-160 (link)

    P1 CalculationTestCase.UnitConfig silently ignored after removing unitConfigLineAccessor

    unitConfigLineAccessor was removed along with the branch that wrapped the line accessor when tc.UnitConfig != nil. The CalculationTestCase.UnitConfig field still exists in the struct, so callers can set it — but RunCalculationTestCase now silently ignores it and always calls GenerateProgressiveBilledDetailedLines with the bare line. Any test that relies on unit-config-aware rounding routed through this helper will compute with a zero unit config, producing incorrect assertions without any compile-time or runtime warning.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: openmeter/billing/rating/service/testutil/ubptest.go
    Line: 56-160
    
    Comment:
    **`CalculationTestCase.UnitConfig` silently ignored after removing `unitConfigLineAccessor`**
    
    `unitConfigLineAccessor` was removed along with the branch that wrapped the line accessor when `tc.UnitConfig != nil`. The `CalculationTestCase.UnitConfig` field still exists in the struct, so callers can set it — but `RunCalculationTestCase` now silently ignores it and always calls `GenerateProgressiveBilledDetailedLines` with the bare line. Any test that relies on unit-config-aware rounding routed through this helper will compute with a zero unit config, producing incorrect assertions without any compile-time or runtime warning.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

  4. openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go, line 366-377 (link)

    P2 SnapshotLineQuantity call removed without comment or replacement in updateMutableStandardInvoice

    The block that called u.quantitySnapshotter.SnapshotLineQuantity(...) and replaced targetStandardLine with the re-snapshotted line was deleted entirely. This means that when a subscription sync applies a period or metadata change to a mutable standard invoice line, the metered quantity is no longer re-calculated before the line is put back into the invoice. Clarify where (or whether) re-snapshotting now happens for this code path.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go
    Line: 366-377
    
    Comment:
    **`SnapshotLineQuantity` call removed without comment or replacement in `updateMutableStandardInvoice`**
    
    The block that called `u.quantitySnapshotter.SnapshotLineQuantity(...)` and replaced `targetStandardLine` with the re-snapshotted line was deleted entirely. This means that when a subscription sync applies a period or metadata change to a mutable standard invoice line, the metered quantity is no longer re-calculated before the line is put back into the invoice. Clarify where (or whether) re-snapshotting now happens for this code path.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

Fix All in Claude Code Fix All in Codex

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

---

### Issue 1 of 6
openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go:94-133
**Incomplete migration in `AddShrink` — still uses old field accessors**

`AddShrink` was only partially migrated. After the call to `existingHierarchy.Lines()` (which now returns `[]LineHeaderAccessor`), the loop body still accesses `child.Line.*` and `child.Invoice.*` — fields that no longer exist on the `LineHeaderAccessor` interface. Critically, the method also calls `child.Line.CloneWithoutChildren()`, `child.Line.UpdateServicePeriod()`, and casts `child.Invoice` to check invoice type, all of which require the concrete `billing.LineWithInvoiceHeader` type. Beyond the compile failure, the `LineHeaderAccessor` interface is intentionally read-only header data, so the mutation logic (`SetDeletedAt`, `UpdateServicePeriod`, `InvoiceAtAccessor`) needs to be re-thought for this path rather than just swapping field access.

### Issue 2 of 6
openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go:164-195
**`AddExtend` not migrated — still uses old `Lines` slice type**

`AddExtend` still accesses `existingHierarchy.Lines` directly as the old `billing.LinesWithInvoiceHeaders` slice (type `[]billing.LineWithInvoiceHeader`), uses `billing.LineWithInvoiceHeader` in the `slices.SortFunc` comparator, and calls `lines[…].Line.CloneWithoutChildren()` and `lines[…].Invoice.AsInvoice().Type()`. None of these exist on the new typed fields (`StandardLines []StandardLine`, `GatheringLine *GatheringLine`). Unlike `AddDelete` and `AddShrink` which were at least partially migrated to `group.Lines()`, this method was not touched at all and needs equivalent migration plus logic to handle the typed `StandardLines`/`GatheringLine` split.

### Issue 3 of 6
openmeter/billing/rating/service/testutil/ubptest.go:56-160
**`CalculationTestCase.UnitConfig` silently ignored after removing `unitConfigLineAccessor`**

`unitConfigLineAccessor` was removed along with the branch that wrapped the line accessor when `tc.UnitConfig != nil`. The `CalculationTestCase.UnitConfig` field still exists in the struct, so callers can set it — but `RunCalculationTestCase` now silently ignores it and always calls `GenerateProgressiveBilledDetailedLines` with the bare line. Any test that relies on unit-config-aware rounding routed through this helper will compute with a zero unit config, producing incorrect assertions without any compile-time or runtime warning.

### Issue 4 of 6
openmeter/billing/charges/usagebased/service/rating/delta/engine_test.go:344-348
**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.

### Issue 5 of 6
openmeter/billing/invoicing/legacy/splitlinegroup/invoicelinesplitgroup.go:200-240
**`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.

### Issue 6 of 6
openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go:366-377
**`SnapshotLineQuantity` call removed without comment or replacement in `updateMutableStandardInvoice`**

The block that called `u.quantitySnapshotter.SnapshotLineQuantity(...)` and replaced `targetStandardLine` with the re-snapshotted line was deleted entirely. This means that when a subscription sync applies a period or metadata change to a mutable standard invoice line, the metered quantity is no longer re-calculated before the line is put back into the invoice. Clarify where (or whether) re-snapshotting now happens for this code path.

Reviews (1): Last reviewed commit: "feat: move legacy splitline handling to ..." | Re-trigger Greptile

Greptile also left 2 inline comments 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: ca68560e-b2ea-482d-bf3e-697bcc1f5b88

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

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