Skip to content

refactor(notification): persist invoice api response#3985

Merged
gergely-kurucz-konghq merged 3 commits intomainfrom
refactor/OM-203-persist-invoice-api-response
Mar 23, 2026
Merged

refactor(notification): persist invoice api response#3985
gergely-kurucz-konghq merged 3 commits intomainfrom
refactor/OM-203-persist-invoice-api-response

Conversation

@gergely-kurucz-konghq
Copy link
Contributor

@gergely-kurucz-konghq gergely-kurucz-konghq commented Mar 20, 2026

What

Decouple invoice notification events from the internal billing.EventStandardInvoice type by persisting new events as api.Invoice instead.

Why

The internal billing type was leaking into the notification persistence layer, requiring lazy transformation at every read site (Svix dispatch, HTTP API). This is the first step in a broader invoice type refactoring — decoupling the notification layer first limits the blast radius of that upcoming change.

How

  • Add Version int to EventPayloadMeta (omitempty: absent = legacy v0, 1 = new v1)
  • InvoicePayload now embeds api.Invoice directly; EventPayload.Invoice is *InvoicePayload
  • Transform billing.EventStandardInvoice → api.Invoice eagerly in the consumer before persisting, mirroring how entitlement events already work
  • Two-pass versioned deserialization in eventPayloadFromJSON: v0 events are transformed on read (no data loss), v1 events pass through directly
  • FromEventAsInvoice*Payload in httpdriver and eventAsPayload in the reconciler become trivial pass-throughs — no functional change to Svix or API output

DB contains a mix of v0 and v1 events during the rollout window; both are handled transparently.

Step 2 (migration of legacy v0 events) and Step 3 (removal of the v0 read path) follow separately.

Summary by CodeRabbit

  • New Features

    • Payload versioning for notification events and automatic conversion of legacy invoice formats to the current API invoice shape.
  • Bug Fixes

    • Improved validation and clearer errors for missing or unsupported invoice payloads.
    • Notification handlers now use the unified API invoice representation for consistent downstream processing.
  • Tests

    • Extensive tests for legacy/current JSON shapes, version handling, conversion, and negative cases (missing keys, unsupported versions).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c584764f-a27c-46dd-a9ad-83b1dc0db59c

📥 Commits

Reviewing files that changed from the base of the PR and between eb1491e and 7ba6cdb.

📒 Files selected for processing (8)
  • openmeter/notification/adapter/entitymapping.go
  • openmeter/notification/adapter/entitymapping_test.go
  • openmeter/notification/consumer/invoice.go
  • openmeter/notification/eventpayload.go
  • openmeter/notification/httpdriver/mapping.go
  • openmeter/notification/httpdriver/mapping_test.go
  • openmeter/notification/internal/rule.go
  • openmeter/notification/invoice.go
✅ Files skipped from review due to trivial changes (1)
  • openmeter/notification/httpdriver/mapping_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • openmeter/notification/invoice.go
  • openmeter/notification/eventpayload.go
  • openmeter/notification/adapter/entitymapping_test.go

📝 Walkthrough

Walkthrough

Adds versioned notification event payloads and a version-aware JSON parser that decodes legacy v0 billing invoice shapes by mapping them to the API invoice model, preserves current v1 payloads, updates consumers/constructors to use API-mapped invoices, and adds unit tests for multiple shapes and error cases.

Changes

Cohort / File(s) Summary
Event Payload Core
openmeter/notification/eventpayload.go, openmeter/notification/invoice.go
Introduce version in EventPayloadMeta with EventPayloadVersionLegacy/Current; replace invoice payload type with InvoicePayload (wraps api.Invoice) and add validation helpers.
Version-aware JSON parsing
openmeter/notification/adapter/entitymapping.go, openmeter/notification/adapter/entitymapping_test.go
Add unexported eventPayloadFromJSON that reads meta, branches by type/version, supports v0 legacy invoice deserialization + mapping to API (upgrades to current), enforces unsupported-version errors; tests cover v0/v1 shapes, missing invoice, unsupported versions, and non-invoice types.
Event construction & handlers
openmeter/notification/consumer/invoice.go, openmeter/notification/internal/rule.go
Map incoming billing event invoices to API invoices via billinghttp.MapEventInvoiceToAPI; set payload Version to current and wrap mapped API invoice in InvoicePayload.
HTTP mapping & tests
openmeter/notification/httpdriver/mapping.go, openmeter/notification/httpdriver/mapping_test.go
Remove billinghttp dependency from HTTP mapping; mapping funcs now read invoice data from InvoicePayload.Invoice directly; add tests to verify payload Data propagation and nil handling.
Adapter mapping change
openmeter/notification/adapter/entitymapping.go (consumer-facing change)
EventFromDBEntity now uses eventPayloadFromJSON and surfaces its specific errors rather than only the original unmarshal error.

Sequence Diagram

sequenceDiagram
    participant Consumer as Event Consumer
    participant Entity as Entity Mapping
    participant Parser as eventPayloadFromJSON
    participant Legacy as Legacy Mapper (billinghttp.MapEventInvoiceToAPI)
    participant Current as Current Parser (v1)
    participant Validator as Validator

    Consumer->>Entity: EventFromDBEntity(raw_json)
    Entity->>Parser: eventPayloadFromJSON(raw_json)
    Parser->>Parser: unmarshal meta (type, version)
    alt type == invoice and version == 0
        Parser->>Legacy: unmarshal v0 envelope -> billing.EventStandardInvoice
        Legacy->>Legacy: MapEventInvoiceToAPI -> api.Invoice
        Legacy-->>Parser: EventPayload{Invoice: InvoicePayload(api.Invoice), Version:1}
    else type == invoice and version == 1
        Parser->>Current: unmarshal directly into EventPayload (InvoicePayload)
        Current-->>Parser: EventPayload{Invoice: InvoicePayload(...), Version:1}
    else non-invoice
        Parser->>Parser: single-pass unmarshal to EventPayload
    end
    Parser-->>Entity: EventPayload
    Entity->>Validator: payload.Validate()
    Validator-->>Entity: ok / error
    Entity-->>Consumer: Event (or error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

area/notification, area/billing

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% 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 directly describes the main change: decoupling and persisting invoice notifications as API responses instead of internal billing types.

✏️ 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/OM-203-persist-invoice-api-response

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.

@gergely-kurucz-konghq gergely-kurucz-konghq added release-note/ignore Ignore this change when generating release notes kind/refactor Code refactor, cleanup or minor improvement labels Mar 20, 2026
@gergely-kurucz-konghq gergely-kurucz-konghq changed the title Refactor/om 203 persist invoice api response refactor(notification): persist invoice api response Mar 20, 2026
Copy link
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: 2

🤖 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/notification/adapter/entitymapping.go`:
- Around line 132-158: The legacy v0 branch populates only payload.Invoice and
thus drops the metadata (Type/Version); after mapping the legacy invoice to
apiInvoice, copy the incoming meta into the returned payload (e.g., set
payload.EventPayloadMeta = meta or assign payload.Type = meta.Type and
payload.Version = meta.Version) so the resulting notification.EventPayload
preserves Type and Version alongside payload.Invoice.

In `@openmeter/notification/invoice.go`:
- Around line 26-27: InvoicePayload.Validate currently returns nil, removing
critical validation that billing.EventStandardInvoice.Validate used to perform;
restore validation by implementing InvoicePayload.Validate to validate the
invoice shape and associated app payloads (reusing or delegating to the previous
billing.EventStandardInvoice.Validate logic where possible), ensure it checks
required invoice fields and app payload integrity, and return any validation
error instead of nil so invoice events routed via eventpayload.go are properly
validated before persistence/readback.
🪄 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: a90f7466-350d-456a-8650-f38bcd94a404

📥 Commits

Reviewing files that changed from the base of the PR and between fb0b4a2 and b3c9dcc.

📒 Files selected for processing (8)
  • openmeter/notification/adapter/entitymapping.go
  • openmeter/notification/adapter/entitymapping_test.go
  • openmeter/notification/consumer/invoice.go
  • openmeter/notification/eventpayload.go
  • openmeter/notification/httpdriver/mapping.go
  • openmeter/notification/httpdriver/mapping_test.go
  • openmeter/notification/internal/rule.go
  • openmeter/notification/invoice.go

Copy link
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 (3)
openmeter/notification/adapter/entitymapping_test.go (3)

99-103: Consider asserting Type and Version are preserved too.

The test verifies the Invoice content round-trips correctly, but doesn't check that payload.Type and payload.Version match original. Adding those assertions would catch regressions in meta preservation.

💡 Suggested addition
 	payload, err := eventPayloadFromJSON(data)
 	require.NoError(t, err)

+	assert.Equal(t, original.Type, payload.Type)
+	assert.Equal(t, original.Version, payload.Version)
 	require.NotNil(t, payload.Invoice)
 	assert.Equal(t, original.Invoice.Invoice, payload.Invoice.Invoice)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/notification/adapter/entitymapping_test.go` around lines 99 - 103,
The test currently checks invoice round-tripping but misses metadata assertions;
update the test that calls eventPayloadFromJSON (where variables original and
payload are used) to also assert that payload.Type == original.Type and
payload.Version == original.Version (e.g., add require.Equal/ assert.Equal
checks for Type and Version after deserialization) so meta fields are preserved.

106-119: Good error case coverage!

These tests nail down the expected error messages for edge cases. Quick thought: you could also add a test for completely malformed JSON (e.g., [] or {invalid}) to verify the first-pass meta unmarshal fails gracefully.

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

In `@openmeter/notification/adapter/entitymapping_test.go` around lines 106 - 119,
Add a new test that checks eventPayloadFromJSON returns an error for completely
malformed JSON (e.g., `[]` and `{invalid}`) to validate the initial meta
unmarshal failure path; create a test function alongside
TestEventPayloadFromJSON_V0MissingInvoice and
TestEventPayloadFromJSON_UnknownVersion (e.g.,
TestEventPayloadFromJSON_MalformedJSON) that calls eventPayloadFromJSON with
malformed payloads and asserts an error is returned and contains an appropriate
message indicating JSON/unmarshal failure.

121-137: Non-invoice path coverage looks good.

One tiny addition that'd be nice: asserting payload.Type == notification.EventTypeBalanceThreshold to verify the type doesn't get lost in the single-pass unmarshal path.

💡 Suggested addition
 	payload, err := eventPayloadFromJSON(data)
 	require.NoError(t, err)

+	assert.Equal(t, notification.EventTypeBalanceThreshold, payload.Type)
 	assert.NotNil(t, payload.BalanceThreshold)
 	assert.Nil(t, payload.Invoice)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/notification/adapter/entitymapping_test.go` around lines 121 - 137,
Add an assertion in TestEventPayloadFromJSON_NonInvoiceType to verify the
unmarshalled payload preserves the event type: after calling
eventPayloadFromJSON and before other assertions, assert that payload.Type ==
notification.EventTypeBalanceThreshold (use assert.Equal or require.Equal
consistent with the file) so the single-pass unmarshal path doesn't drop the
Type field.
🤖 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/notification/adapter/entitymapping_test.go`:
- Around line 99-103: The test currently checks invoice round-tripping but
misses metadata assertions; update the test that calls eventPayloadFromJSON
(where variables original and payload are used) to also assert that payload.Type
== original.Type and payload.Version == original.Version (e.g., add
require.Equal/ assert.Equal checks for Type and Version after deserialization)
so meta fields are preserved.
- Around line 106-119: Add a new test that checks eventPayloadFromJSON returns
an error for completely malformed JSON (e.g., `[]` and `{invalid}`) to validate
the initial meta unmarshal failure path; create a test function alongside
TestEventPayloadFromJSON_V0MissingInvoice and
TestEventPayloadFromJSON_UnknownVersion (e.g.,
TestEventPayloadFromJSON_MalformedJSON) that calls eventPayloadFromJSON with
malformed payloads and asserts an error is returned and contains an appropriate
message indicating JSON/unmarshal failure.
- Around line 121-137: Add an assertion in
TestEventPayloadFromJSON_NonInvoiceType to verify the unmarshalled payload
preserves the event type: after calling eventPayloadFromJSON and before other
assertions, assert that payload.Type == notification.EventTypeBalanceThreshold
(use assert.Equal or require.Equal consistent with the file) so the single-pass
unmarshal path doesn't drop the Type field.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3b1980bd-cd47-49e1-b158-782583287650

📥 Commits

Reviewing files that changed from the base of the PR and between b3c9dcc and eb1491e.

📒 Files selected for processing (2)
  • openmeter/notification/adapter/entitymapping.go
  • openmeter/notification/adapter/entitymapping_test.go

Copy link
Member

@turip turip left a comment

Choose a reason for hiding this comment

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

Thank you!

@gergely-kurucz-konghq gergely-kurucz-konghq force-pushed the refactor/OM-203-persist-invoice-api-response branch from eb1491e to 7ba6cdb Compare March 23, 2026 13:15
@gergely-kurucz-konghq gergely-kurucz-konghq merged commit 91cdcc7 into main Mar 23, 2026
23 checks passed
@gergely-kurucz-konghq gergely-kurucz-konghq deleted the refactor/OM-203-persist-invoice-api-response branch March 23, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/refactor Code refactor, cleanup or minor improvement release-note/ignore Ignore this change when generating release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants