Skip to content

fix: invoiceline splitgroup is not marshalable#3875

Merged
turip merged 3 commits intomainfrom
fix/invoiceline-splitgroup-marshaling
Feb 18, 2026
Merged

fix: invoiceline splitgroup is not marshalable#3875
turip merged 3 commits intomainfrom
fix/invoiceline-splitgroup-marshaling

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented Feb 18, 2026

Overview

Fixes event marshaling error:
json: cannot unmarshal object into Go struct field StandardInvoice.old.invoice.lines.progressiveLineHierarchy.Lines.Line of type billing.GenericInvoiceLine

Notes for reviewer

Summary by CodeRabbit

  • Refactor

    • Improved JSON serialization/deserialization for invoice line structures, adding multi-variant support, explicit marshaling/unmarshaling logic, and robust error handling to maintain a consistent public data shape.
  • Tests

    • Added a unit test to verify round-trip JSON marshaling/unmarshaling of invoice line hierarchies across supported invoice variants.

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

coderabbitai Bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Adds JSON marshal/unmarshal for LineWithInvoiceHeader via a generic serde that discriminates between Standard and Gathering invoice/line variants, with explicit (de)serialization logic and error handling; includes a unit test that verifies JSON round-trip fidelity for SplitLineHierarchy.

Changes

Cohort / File(s) Summary
LineWithInvoiceHeader serde
openmeter/billing/invoicelinesplitgroup.go
Adds lineWithInvoiceHeaderSerde generic type and implements MarshalJSON and UnmarshalJSON on LineWithInvoiceHeader, performing type-discriminated handling for Standard and Gathering invoice/line variants and adding error paths for unknown/invalid types. Also imports encoding/json.
Round-trip test
openmeter/billing/invoicelinesplitgroup_test.go
Adds TestSplitLineGroupMarshaling to construct a SplitLineHierarchy with both Standard and Gathering entries, marshal to JSON and unmarshal back, and assert deep equality to verify round-trip serialization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'fix: invoiceline splitgroup is not marshalable' directly describes the main fix in the changeset: adding JSON serialization support to make LineWithInvoiceHeader marshalable/unmarshalable.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/invoiceline-splitgroup-marshaling

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.

@turip turip added release-note/bug-fix Release note: Bug Fixes area/billing labels Feb 18, 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 (2)
openmeter/billing/invoicelinesplitgroup.go (1)

241-282: Consider adding nil guards for l.Invoice and l.Line before dereferencing.

If MarshalJSON is ever called on a zero-value or partially-initialized LineWithInvoiceHeader, lines 242 and 253/269 will panic with a nil interface method call. A quick guard at the top would make this more resilient.

Also a small style nit: UnmarshalJSON uses a default: case in its switch, while MarshalJSON falls through to the error return after the switch. Using the same pattern in both would be a bit more readable.

Suggested nil guard + consistent switch
 func (l *LineWithInvoiceHeader) MarshalJSON() ([]byte, error) {
+	if l.Invoice == nil || l.Line == nil {
+		return nil, fmt.Errorf("cannot marshal LineWithInvoiceHeader: Invoice and Line must not be nil")
+	}
+
 	invoice := l.Invoice.AsInvoice()
 
 	invoiceType := invoice.Type()
 
 	switch invoiceType {
 	case InvoiceTypeStandard:
 		// ... unchanged ...
 	case InvoiceTypeGathering:
 		// ... unchanged ...
+	default:
+		return nil, fmt.Errorf("unknown invoice type: %s", invoiceType)
 	}
-
-	return nil, fmt.Errorf("unknown invoice type: %s", invoiceType)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/invoicelinesplitgroup.go` around lines 241 - 282, Add nil
guards at the start of LineWithInvoiceHeader.MarshalJSON to check that l,
l.Invoice and l.Line are non-nil and return a clear error if any are nil to
avoid panics when calling l.Invoice.AsInvoice() or l.Line.AsInvoiceLine(); then
refactor the switch on invoiceType to include a default: case that returns the
unknown-invoice-type error (matching UnmarshalJSON style) rather than falling
through to a post-switch return. Ensure you reference the existing methods
Invoice.AsInvoice, Line.AsInvoiceLine, AsStandardInvoice/AsGatheringInvoice and
AsStandardLine/AsGatheringLine when adding the guards and error paths.
openmeter/billing/invoicelinesplitgroup_test.go (1)

72-87: Nice round-trip test — covers the core fix well.

Two small suggestions:

  1. Style: You're mixing t.Fatalf (lines 74, 82) with require (line 87). Since require is already imported, using it consistently keeps things tidy:
-	jsonBytes, err := json.Marshal(testGroup)
-	if err != nil {
-		t.Fatalf("failed to marshal test group: %v", err)
-	}
+	jsonBytes, err := json.Marshal(testGroup)
+	require.NoError(t, err, "failed to marshal test group")
 
 	t.Logf("test group: %s", string(jsonBytes))
 
-	var unmarshalled SplitLineHierarchy
-	err = json.Unmarshal(jsonBytes, &unmarshalled)
-	if err != nil {
-		t.Fatalf("failed to unmarshal test group: %v", err)
-	}
+	var unmarshalled SplitLineHierarchy
+	err = json.Unmarshal(jsonBytes, &unmarshalled)
+	require.NoError(t, err, "failed to unmarshal test group")
  1. Coverage: Consider adding a sub-test for unmarshaling with an unknown "type" value (e.g., "type":"bogus") to exercise the error path in UnmarshalJSON. A quick negative test goes a long way for confidence.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/invoicelinesplitgroup_test.go` around lines 72 - 87,
Replace the manual t.Fatalf checks around json.Marshal/json.Unmarshal with
require assertions (e.g., require.NoError(t, err) or require.NoErrorf) to be
consistent with the existing require usage, and add a small sub-test that
attempts to json.Unmarshal a payload into SplitLineHierarchy with an unknown
"type" value (e.g., "type":"bogus") and asserts that UnmarshalJSON returns an
error (using require.Error) to cover the negative path in UnmarshalJSON.
🤖 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/invoicelinesplitgroup_test.go`:
- Around line 72-87: Replace the manual t.Fatalf checks around
json.Marshal/json.Unmarshal with require assertions (e.g., require.NoError(t,
err) or require.NoErrorf) to be consistent with the existing require usage, and
add a small sub-test that attempts to json.Unmarshal a payload into
SplitLineHierarchy with an unknown "type" value (e.g., "type":"bogus") and
asserts that UnmarshalJSON returns an error (using require.Error) to cover the
negative path in UnmarshalJSON.

In `@openmeter/billing/invoicelinesplitgroup.go`:
- Around line 241-282: Add nil guards at the start of
LineWithInvoiceHeader.MarshalJSON to check that l, l.Invoice and l.Line are
non-nil and return a clear error if any are nil to avoid panics when calling
l.Invoice.AsInvoice() or l.Line.AsInvoiceLine(); then refactor the switch on
invoiceType to include a default: case that returns the unknown-invoice-type
error (matching UnmarshalJSON style) rather than falling through to a
post-switch return. Ensure you reference the existing methods Invoice.AsInvoice,
Line.AsInvoiceLine, AsStandardInvoice/AsGatheringInvoice and
AsStandardLine/AsGatheringLine when adding the guards and error paths.

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/invoicelinesplitgroup.go (2)

241-282: Optional: nil guards + default case tidiness

Two small things worth considering:

  1. Nil check: if someone accidentally marshals a zero-value LineWithInvoiceHeader{}, l.Invoice.AsInvoice() (and similarly l.Line.AsInvoiceLine()) will panic. A quick guard at the top keeps things tidy:
🛡️ Suggested nil guard
 func (l LineWithInvoiceHeader) MarshalJSON() ([]byte, error) {
+	if l.Invoice == nil || l.Line == nil {
+		return nil, fmt.Errorf("LineWithInvoiceHeader has nil Invoice or Line")
+	}
 	invoice := l.Invoice.AsInvoice()
  1. default: case: the switch in MarshalJSON falls through to the error after the closing brace, which is correct — but having an explicit default: mirrors the style in UnmarshalJSON and makes exhaustiveness more obvious:
♻️ Suggested default case
 	case InvoiceTypeGathering:
 		...
 		return json.Marshal(...)
+	default:
+		return nil, fmt.Errorf("unknown invoice type: %s", invoiceType)
 	}
-
-	return nil, fmt.Errorf("unknown invoice type: %s", invoiceType)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/invoicelinesplitgroup.go` around lines 241 - 282, Add a
nil-guard at the start of LineWithInvoiceHeader.MarshalJSON to avoid panics when
called on a zero-value (check that l.Invoice and l.Line are non-nil/valid before
calling l.Invoice.AsInvoice() and l.Line.AsInvoiceLine() and return a clear
error if missing), and make the switch on invoiceType explicitly exhaustive by
adding a default: branch that returns the same fmt.Errorf("unknown invoice type:
%s", invoiceType) (this mirrors UnmarshalJSON style and prevents fall-through
ambiguity); update MarshalJSON accordingly around the existing calls to
AsInvoice, AsStandardInvoice/AsGatheringInvoice and
AsInvoiceLine/AsStandardLine/AsGatheringLine.

206-239: Quick heads-up: Invoice has different dynamic types depending on construction path

Just noticed that NewLineWithInvoiceHeader and UnmarshalJSON store the invoice differently:

  • NewLineWithInvoiceHeader stores the value directly (e.g., StandardInvoice)
  • UnmarshalJSON stores a pointer (e.g., *StandardInvoice)

Both satisfy the GenericInvoiceReader interface, so it works fine, and I didn't find any type assertions that would break. But if someone adds a type assertion down the road, it could silently fail after a JSON round-trip. Worth keeping an eye on if you're refactoring this area.

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

In `@openmeter/billing/invoicelinesplitgroup.go` around lines 206 - 239,
UnmarshalJSON currently sets l.Invoice to a pointer (e.g.,
&unmarshalled.Invoice) while NewLineWithInvoiceHeader stores invoice values
directly, causing inconsistent dynamic types; make UnmarshalJSON assign the
invoice value the same way as NewLineWithInvoiceHeader by setting l.Invoice =
unmarshalled.Invoice (for both StandardInvoice and GatheringInvoice branches) so
l.Invoice consistently holds the same concrete type shape; update both branches
in UnmarshalJSON (references: UnmarshalJSON, lineWithInvoiceHeaderSerde,
StandardInvoice, GatheringInvoice, l.Invoice) accordingly.
🤖 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/invoicelinesplitgroup.go`:
- Around line 241-282: Add a nil-guard at the start of
LineWithInvoiceHeader.MarshalJSON to avoid panics when called on a zero-value
(check that l.Invoice and l.Line are non-nil/valid before calling
l.Invoice.AsInvoice() and l.Line.AsInvoiceLine() and return a clear error if
missing), and make the switch on invoiceType explicitly exhaustive by adding a
default: branch that returns the same fmt.Errorf("unknown invoice type: %s",
invoiceType) (this mirrors UnmarshalJSON style and prevents fall-through
ambiguity); update MarshalJSON accordingly around the existing calls to
AsInvoice, AsStandardInvoice/AsGatheringInvoice and
AsInvoiceLine/AsStandardLine/AsGatheringLine.
- Around line 206-239: UnmarshalJSON currently sets l.Invoice to a pointer
(e.g., &unmarshalled.Invoice) while NewLineWithInvoiceHeader stores invoice
values directly, causing inconsistent dynamic types; make UnmarshalJSON assign
the invoice value the same way as NewLineWithInvoiceHeader by setting l.Invoice
= unmarshalled.Invoice (for both StandardInvoice and GatheringInvoice branches)
so l.Invoice consistently holds the same concrete type shape; update both
branches in UnmarshalJSON (references: UnmarshalJSON,
lineWithInvoiceHeaderSerde, StandardInvoice, GatheringInvoice, l.Invoice)
accordingly.

@turip turip merged commit e66feb3 into main Feb 18, 2026
24 of 25 checks passed
@turip turip deleted the fix/invoiceline-splitgroup-marshaling branch February 18, 2026 03:36
@tothandras
Copy link
Copy Markdown
Contributor

approved ✅

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants