Skip to content

refactor: prepare for charges integration#4023

Merged
turip merged 8 commits into
mainfrom
refactor/targetstate-item-v2
Mar 28, 2026
Merged

refactor: prepare for charges integration#4023
turip merged 8 commits into
mainfrom
refactor/targetstate-item-v2

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented Mar 28, 2026

Overview

This refactor introduces a package-owned persisted-state abstraction for subscription sync, replacing direct billing.LineOrHierarchy usage with typed persistedstate.Item wrappers and moving referenced invoice loading into
the persisted snapshot itself.

This allows us to add charges as one of these Items, thus charges updates will be driver via a different codepath without the danger of changing the existing flow.

It also restructures reconciliation around per-persisted-shape invoice patch collections, so line-backed and split-line-hierarchy-backed states are patched through separate direct-billing
paths while preserving current behavior.

Along the way, target filtering for direct billing was made explicit, unknown persisted invoices now surface errors instead of being silently assumed gathering, and the progressive
billing cancellation path was fixed so emptied hierarchy children are deleted rather than updated.

Detailed changes

The main differences introduced by this refactor in subscriptionsync are:

  • Persisted-state boundary is now package-owned instead of exposing raw billing.LineOrHierarchy.
    See openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go, openmeter/billing/worker/subscriptionsync/service/persistedstate/state.go, openmeter/billing/worker/subscriptionsync/service/persistedstate/
    loader.go.
  • Persisted invoices are now part of persistedstate.State, and only invoices referenced by loaded persisted entities are fetched.
    This is a real behavior change versus main, which loaded broader customer invoice state.
    Missing referenced invoices now fail explicitly instead of being tolerated.
  • Invoices.IsGatheringInvoice(...) now returns an error when an invoice is unknown instead of assuming “gathering”.
    That is a meaningful safety change.
  • Reconciler planning no longer builds deferred concrete patch structs like CreatePatch, ShrinkUsageBasedPatch, ProratePatch.
    It now emits realized invoice patches through per-shape collections.
    See openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go, openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoice.go, openmeter/billing/worker/subscriptionsync/service/reconciler/
    patchinvoiceline.go, openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go.
  • Reconciler is now routed by persisted item type.
    Existing line-backed and hierarchy-backed states go through separate patching logic, which is the main architectural change for future charge support.
  • semanticProrateDecision moved out into openmeter/billing/worker/subscriptionsync/service/reconciler/prorate.go and now owns expected-line rendering itself.
  • diffItem(...) no longer depends on rendered line state for period comparison.
    It now uses target-state service period directly via openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go.
  • Direct billing sync now filters target items before diffing:
    non-billable items and items whose GetExpectedLine() is nil are removed from invoice-sync scope up front.
    See openmeter/billing/worker/subscriptionsync/service/reconciler/reconciler.go.
  • Targetstate annotation handling no longer branches on raw billing types.
    The persisted-item abstraction now owns “subscription managed” and “last line annotation” semantics.
    See openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go and openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go.
  • The hierarchy shrink path now deletes an emptied usage-based child instead of updating it.
    That was a real behavior fix validated by the progressive billing cancellation scenario.

Fixes #(issue)

Notes for reviewer

Summary by CodeRabbit

  • New Features

    • Invoice listing can be filtered by specific invoice IDs.
    • Added a persisted-item abstraction and a prorate decision type for reconciliation.
  • Bug Fixes

    • Improved error handling when invoice lookup/missing invoice occurs and when detecting duplicate persisted items.
    • IsGatheringInvoice now surfaces errors instead of assuming gathering.
  • Refactor

    • Reworked subscription reconciliation to invoice-scoped patch planning with new patch-collection routing and invoice-focused state storage.

@turip turip requested a review from a team as a code owner March 28, 2026 18:03
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

Adds invoice-ID filtering to listing and refactors subscription-sync reconciliation: introduces a persisted Item abstraction, moves persisted invoices into state, replaces legacy patch types with invoice-scoped patch collections and a router, and routes invoice-listing by IDs through ListInvoices.

Changes

Cohort / File(s) Summary
Invoice ID Filtering
openmeter/billing/invoice.go, openmeter/billing/service/invoice.go
Added IDs []string to ListInvoicesInput and propagated it to adapter input to allow filtering invoices by their IDs.
Persisted State Item Abstraction
openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go
New Item interface and concrete wrappers for lines and split-line hierarchies with accessors and conversion helpers (ItemAsLine, ItemAsSplitLineHierarchy, NewItemFromLineOrHierarchy).
Persisted State Loading & Structure
openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go, .../state.go
LoadForSubscription now constructs Items, enforces unique ChildUniqueReferenceID, populates State.Invoices (removed Lines), and loads invoices via ListInvoices(..., IDs, IncludeDeleted). State.ByUniqueID maps to Item; Invoices type changed to map[string]billing.Invoice; IsGatheringInvoice now returns (bool, error).
Patch system core refactor
openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go, .../patchinvoice.go
Removed GetInvoicePatchesInput; introduced InvoicePatch, InvoicePatchCollection, PatchCollection interfaces, patchCollectionRouter, and an invoicePatchCollectionBase helper for collecting invoice patches.
Line-based invoice patching
openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoiceline.go, .../patchhelpers.go
New lineInvoicePatchCollection implementing create/delete/shrink/extend/prorate for line items; getPatchesForUpdateUsageBasedLine now returns a single patch and surfaces invoice lookup errors.
Hierarchy-based invoice patching
openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go
New lineHierarchyPatchCollection handling split-line hierarchies (delete/shrink/extend), rejects unsupported ops, emits group + per-line patches, and respects annotations and gathering-invoice rules.
Removed legacy patch implementations
openmeter/billing/worker/subscriptionsync/service/reconciler/*.go
Deleted older concrete patch types (CreatePatch, DeletePatch, ExtendUsageBasedPatch, ProratePatch, ShrinkUsageBasedPatch) — logic moved into the collection-based handlers.
Proration decision helper
openmeter/billing/worker/subscriptionsync/service/reconciler/prorate.go
Added ProrateDecision and semanticProrateDecision to decide flat-fee proration and report original/target amounts.
Reconciler core changes
openmeter/billing/worker/subscriptionsync/service/reconciler/reconciler.go
Plan now holds InvoicePatches []InvoicePatch and Invoices; diffing mutates a PatchCollection via AddCreate/AddDelete/AddShrink/AddExtend/AddProrate; added filter pre-pass and patch routing via router.
Target state adjustments
openmeter/billing/worker/subscriptionsync/service/targetstate/...
Replaced annotation-traversal helpers with persisted-item methods (IsSubscriptionManaged(), HasLastLineAnnotation()); added StateItem.GetServicePeriod().
Sync service
openmeter/billing/worker/subscriptionsync/service/sync.go
Removed persisted-invoice loading from SynchronizeSubscription; reconciler plan now carries invoices.

Sequence Diagram(s)

sequenceDiagram
  participant Service as Subscription Service
  participant Loader as PersistedState Loader
  participant InvoiceSvc as Billing/ListInvoices
  participant Reconciler as Reconciler (Plan/Apply)
  participant Router as PatchCollection Router
  participant Updater as Invoice Updater

  Service->>Loader: LoadForSubscription(subscription)
  Loader->>InvoiceSvc: ListInvoices(Namespaces, IDs, IncludeDeleted)
  InvoiceSvc-->>Loader: invoices map
  Loader-->>Service: State{ByUniqueID: Items, Invoices}
  Service->>Reconciler: Plan(state, targets)
  Reconciler->>Router: newPatchCollectionRouter(invoices)
  Reconciler->>Router: route items -> PatchCollection
  Router-->>Reconciler: InvoicePatches
  Reconciler->>Updater: Apply(InvoicePatches)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

area/billing, area/subscriptions

Suggested reviewers

  • tothandras
  • GAlexIHU
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "refactor: prepare for charges integration" accurately captures the main architectural refactor of the PR—restructuring persisted-state abstractions and reconciler patch handling to lay groundwork for charges integration—and is appropriately concise.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 refactor/targetstate-item-v2

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoice.go (1)

41-43: Optional hardening: return defensive copies of patch slices.

Returning internal slices directly can let callers mutate collection internals by accident.

Possible tweak
 func (c invoicePatchCollectionBase) Patches() []InvoicePatch {
-	return c.patches
+	return append([]InvoicePatch(nil), c.patches...)
 }
@@
 func (p genericInvoicePatch) GetInvoicePatches() ([]invoiceupdater.Patch, error) {
-	return p.invoiceUpdates, nil
+	return append([]invoiceupdater.Patch(nil), p.invoiceUpdates...), nil
 }

Also applies to: 67-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoice.go`
around lines 41 - 43, The Patches() method on invoicePatchCollectionBase
currently returns the internal slice directly, allowing external mutation;
modify invoicePatchCollectionBase.Patches (and the equivalent method at the
other occurrence) to return a defensive copy of c.patches (e.g., allocate a new
slice, copy elements, and return that) so callers cannot mutate internal state
while preserving the original element values and slice length/capacity
semantics.
openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go (1)

224-231: Consider collapsing duplicate last-line annotation lookups in this loop.

Right now we call HasLastLineAnnotation(...) twice on the same persisted item (ignore + forceContinuous). For hierarchy-backed items this can mean two child scans per iteration; a single-fetch shape (or API that returns both flags together) would keep this path leaner.

As per coding guidelines "Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations ... should be vetted for potential performance bottlenecks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go`
around lines 224 - 231, The loop is calling HasLastLineAnnotation twice on the
same persisted item (existingPreviousLine) which can be expensive for
hierarchy-backed items; change the code to fetch both annotation flags in a
single call or helper so you only scan children once: use
existingPreviousLine.IsSubscriptionManaged() together with a single retrieval
like a new method (e.g., existingPreviousLine.GetLastLineAnnotationFlags or
GetLastLineAnnotations) or a single HasLastLineAnnotation call that returns both
billing.AnnotationSubscriptionSyncIgnore and
billing.AnnotationSubscriptionSyncForceContinuousLines values, then use those
two booleans instead of calling HasLastLineAnnotation twice (references:
existingPreviousLine.IsSubscriptionManaged,
existingPreviousLine.HasLastLineAnnotation,
billing.AnnotationSubscriptionSyncIgnore,
billing.AnnotationSubscriptionSyncForceContinuousLines).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go`:
- Around line 82-86: Guard against a nil Item before performing the type
assertion in ItemAsLine so you don't call getErrorDetails(nil); explicitly check
if in == nil and return a clear error before asserting to LineGetter, and apply
the same nil-check hardening to the other conversion helper in this file (the
other function that asserts to LineGetter around the later block) so
getErrorDetails is only invoked for non-nil values.

In `@openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go`:
- Around line 46-66: The code is building a map byUniqueID from lines but still
calls loadInvoicesForSubscriptionLines(ctx, subs, lines) over the unfiltered
lines; change the call to only pass the rows that were actually added to
byUniqueID (e.g., collect filteredLines while looping or derive them from the
map) so loadInvoicesForSubscriptionLines receives only persisted rows. Update
the call site of loadInvoicesForSubscriptionLines to use the filtered slice (or
the map keys/values) and keep the existing error handling in loader.go around
NewItemFromLineOrHierarchy and byUniqueID checks.

In `@openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go`:
- Around line 71-79: The GetCollectionFor method on patchCollectionRouter
currently returns nil for unknown persistedstate.Item types which leads to a
later panic in diffItem() when it immediately calls Add* on the returned
PatchCollection; change this to fail explicitly by either (A) returning an
erroring implementation of PatchCollection that returns a descriptive error from
its Add* methods (so callers like diffItem() get deterministic errors), or (B)
change the signature of patchCollectionRouter.GetCollectionFor to return
(PatchCollection, error) and return a clear error for unsupported item types,
updating callers (e.g., diffItem()) to handle the error instead of dereferencing
a nil collection.

In
`@openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go`:
- Around line 112-127: When reviving managed child lines you must also revive
the parent split group: after you call existingHierarchy.Group.ToUpdate() (in
both AddShrink and AddExtend) and you perform updatedLine.SetDeletedAt(nil) for
a managed child, set updatedGroup.DeletedAt = nil before creating the
NewUpdateSplitLineGroupPatch; this ensures the group's DeletedAt is cleared
alongside managed children so the parent is not left deleted while children are
active.

---

Nitpick comments:
In
`@openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoice.go`:
- Around line 41-43: The Patches() method on invoicePatchCollectionBase
currently returns the internal slice directly, allowing external mutation;
modify invoicePatchCollectionBase.Patches (and the equivalent method at the
other occurrence) to return a defensive copy of c.patches (e.g., allocate a new
slice, copy elements, and return that) so callers cannot mutate internal state
while preserving the original element values and slice length/capacity
semantics.

In
`@openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go`:
- Around line 224-231: The loop is calling HasLastLineAnnotation twice on the
same persisted item (existingPreviousLine) which can be expensive for
hierarchy-backed items; change the code to fetch both annotation flags in a
single call or helper so you only scan children once: use
existingPreviousLine.IsSubscriptionManaged() together with a single retrieval
like a new method (e.g., existingPreviousLine.GetLastLineAnnotationFlags or
GetLastLineAnnotations) or a single HasLastLineAnnotation call that returns both
billing.AnnotationSubscriptionSyncIgnore and
billing.AnnotationSubscriptionSyncForceContinuousLines values, then use those
two booleans instead of calling HasLastLineAnnotation twice (references:
existingPreviousLine.IsSubscriptionManaged,
existingPreviousLine.HasLastLineAnnotation,
billing.AnnotationSubscriptionSyncIgnore,
billing.AnnotationSubscriptionSyncForceContinuousLines).
🪄 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: bd9d7b51-ef67-4d94-9af5-6c903203248c

📥 Commits

Reviewing files that changed from the base of the PR and between 2fc9af2 and ed012f3.

📒 Files selected for processing (20)
  • openmeter/billing/invoice.go
  • openmeter/billing/service/invoice.go
  • openmeter/billing/worker/subscriptionsync/service/persistedstate/item.go
  • openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go
  • openmeter/billing/worker/subscriptionsync/service/persistedstate/state.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchcreate.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchdelete.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchextend.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchhelpers.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoice.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchprorate.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchshrink.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/prorate.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/reconciler.go
  • openmeter/billing/worker/subscriptionsync/service/sync.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstate.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
💤 Files with no reviewable changes (6)
  • openmeter/billing/worker/subscriptionsync/service/sync.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchdelete.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchcreate.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchshrink.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchprorate.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchextend.go

Comment thread openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go Outdated
@turip turip added the release-note/misc Miscellaneous changes label Mar 28, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go (1)

80-97: Consider adding a default case for future-proofing.

The switch handles the current two LineOrHierarchyType values, but if a new type is ever added, this would silently skip it without collecting its invoice IDs. That could lead to confusing "invoice not found" errors downstream rather than failing at the source.

This is a minor defensive coding suggestion - totally fine to defer if the type enum is considered stable.

🛡️ Optional: Add explicit default case
 		case billing.LineOrHierarchyTypeHierarchy:
 			hierarchy, err := line.AsHierarchy()
 			if err != nil {
 				return Invoices{}, fmt.Errorf("getting hierarchy invoice ids: %w", err)
 			}
 
 			for _, child := range hierarchy.Lines {
 				invoiceIDs[child.Invoice.GetID()] = struct{}{}
 			}
+		default:
+			return Invoices{}, fmt.Errorf("unsupported line type for invoice loading: %s", line.Type())
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go`
around lines 80 - 97, The switch over line.Type() currently handles
billing.LineOrHierarchyTypeLine and billing.LineOrHierarchyTypeHierarchy but
omits a default case; add a default branch to the switch that returns a clear
error (using fmt.Errorf) including the unexpected line.Type() value so the
caller (and the invoiceIDs map population) fails fast when a new
LineOrHierarchyType is introduced; update the existing switch surrounding
invoiceIDs and line (the block that calls
line.AsGenericLine()/line.AsHierarchy() and populates invoiceIDs) to include
this default error-returning case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go`:
- Around line 80-97: The switch over line.Type() currently handles
billing.LineOrHierarchyTypeLine and billing.LineOrHierarchyTypeHierarchy but
omits a default case; add a default branch to the switch that returns a clear
error (using fmt.Errorf) including the unexpected line.Type() value so the
caller (and the invoiceIDs map population) fails fast when a new
LineOrHierarchyType is introduced; update the existing switch surrounding
invoiceIDs and line (the block that calls
line.AsGenericLine()/line.AsHierarchy() and populates invoiceIDs) to include
this default error-returning case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0d930735-e884-4ac5-9494-3ea4a0e179bd

📥 Commits

Reviewing files that changed from the base of the PR and between ed012f3 and ee0b024.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go (1)

46-73: LGTM! Clean refactor of the persisted state construction.

The explicit loop with proper error handling for NewItemFromLineOrHierarchy is much clearer than the previous slicesx.UniqueGroupBy approach. The duplicate ID detection and early-return pattern keeps the logic easy to follow.

One tiny nit: the duplicate error message on line 59 could include the actual ID for easier debugging:

✨ Optional: include duplicate ID in error
 		if _, ok := byUniqueID[*uniqueID]; ok {
-			return State{}, fmt.Errorf("duplicate unique ids in the existing lines")
+			return State{}, fmt.Errorf("duplicate unique ids in the existing lines: %s", *uniqueID)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go`
around lines 46 - 73, The duplicate-ID error message in the loop that builds
byUniqueID (inside the same loader.go loop using uniqueID and
NewItemFromLineOrHierarchy) should include the actual duplicated ID for easier
debugging; update the error returned when detecting an existing key (the branch
that checks if _, ok := byUniqueID[*uniqueID]; ok) to format the message with
*uniqueID (and optionally surrounding context like subscription/line info)
instead of the generic "duplicate unique ids in the existing lines".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openmeter/billing/worker/subscriptionsync/service/persistedstate/loader.go`:
- Around line 46-73: The duplicate-ID error message in the loop that builds
byUniqueID (inside the same loader.go loop using uniqueID and
NewItemFromLineOrHierarchy) should include the actual duplicated ID for easier
debugging; update the error returned when detecting an existing key (the branch
that checks if _, ok := byUniqueID[*uniqueID]; ok) to format the message with
*uniqueID (and optionally surrounding context like subscription/line info)
instead of the generic "duplicate unique ids in the existing lines".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5562e580-ab58-4b03-b9ab-ceca0add029e

📥 Commits

Reviewing files that changed from the base of the PR and between ee0b024 and 398165a.

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

Comment thread openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go Outdated
@turip turip enabled auto-merge (squash) March 28, 2026 20:38
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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/worker/subscriptionsync/service/reconciler/patch.go (1)

88-96: Consider adding a brief comment on when nil patches can appear.

The nil filtering is good defensive coding, but it might help future maintainers to understand when/why a patch entry could be nil. If it's an invariant that should never happen in practice, a comment like // filter defensive: should not occur if collections are constructed correctly would clarify intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go` around
lines 88 - 96, Add a short explanatory comment above the nil-filter in
patchCollectionRouter.CollectInvoicePatches explaining when nil InvoicePatch
entries can appear (e.g., if lineCollection.Patches() or
hierarchyCollection.Patches() may return nil entries during construction/merge
or when a collector fails) or state that nils are a defensive invariant (e.g.,
"// defensive: nil patches may appear if a sub-collector failed; should not
occur with correct construction"). Keep the comment brief and colocate it
immediately before the lo.Filter call referencing InvoicePatch,
lineCollection.Patches, and hierarchyCollection.Patches.
openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go (1)

160-189: In-place sort modifies the original hierarchy's Lines slice.

Line 161 assigns lines := existingHierarchy.Lines which shares the backing array, so the slices.SortFunc on line 162 mutates the original hierarchy. This works fine since the hierarchy isn't reused, but could surprise future maintainers.

A defensive copy would make this safer:

Optional: defensive copy
 if len(existingHierarchy.Lines) > 0 {
-    lines := existingHierarchy.Lines
+    lines := slices.Clone(existingHierarchy.Lines)
     slices.SortFunc(lines, func(i, j billing.LineWithInvoiceHeader) int {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go`
around lines 160 - 189, The code sorts existingHierarchy.Lines in-place (lines
:= existingHierarchy.Lines followed by slices.SortFunc) which mutates the
original hierarchy; make a defensive copy of the slice before sorting (copy into
a new []billing.LineWithInvoiceHeader) so the original existingHierarchy.Lines
is not modified, then run slices.SortFunc on that new slice and continue using
that copied slice for deriving lastChild and creating the
invoiceupdater.NewUpdateLinePatch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go`:
- Around line 88-96: Add a short explanatory comment above the nil-filter in
patchCollectionRouter.CollectInvoicePatches explaining when nil InvoicePatch
entries can appear (e.g., if lineCollection.Patches() or
hierarchyCollection.Patches() may return nil entries during construction/merge
or when a collector fails) or state that nils are a defensive invariant (e.g.,
"// defensive: nil patches may appear if a sub-collector failed; should not
occur with correct construction"). Keep the comment brief and colocate it
immediately before the lo.Filter call referencing InvoicePatch,
lineCollection.Patches, and hierarchyCollection.Patches.

In
`@openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go`:
- Around line 160-189: The code sorts existingHierarchy.Lines in-place (lines :=
existingHierarchy.Lines followed by slices.SortFunc) which mutates the original
hierarchy; make a defensive copy of the slice before sorting (copy into a new
[]billing.LineWithInvoiceHeader) so the original existingHierarchy.Lines is not
modified, then run slices.SortFunc on that new slice and continue using that
copied slice for deriving lastChild and creating the
invoiceupdater.NewUpdateLinePatch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2be4ac4a-0bf5-4d80-b589-7af700374995

📥 Commits

Reviewing files that changed from the base of the PR and between 398165a and 5fd58e0.

📒 Files selected for processing (3)
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patch.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchinvoicelinehierarchy.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/reconciler.go

@turip turip merged commit 3cb4faf into main Mar 28, 2026
23 of 24 checks passed
@turip turip deleted the refactor/targetstate-item-v2 branch March 28, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/misc Miscellaneous changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants