Skip to content

fix(ledger): book advance attribution at purchase time - #4767

Merged
GAlexIHU merged 1 commit into
mainfrom
codex/credit-purchase-attribution-timing
Jul 29, 2026
Merged

fix(ledger): book advance attribution at purchase time#4767
GAlexIHU merged 1 commit into
mainfrom
codex/credit-purchase-attribution-timing

Conversation

@GAlexIHU

@GAlexIHU GAlexIHU commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • keep subscription and schedule definitions ledger-neutral; this changes only the materialized credit-purchase initiation path
  • determine and book existing advance attribution at purchase time
  • keep only the non-attributed remainder as issuance at the grant's future effective_at
  • align the ledger timing with the immediate lineage backfill so a subsequent committed purchase sees the already-settled advance

Behavior

  • a future-effective 100-credit purchase against 40 of existing advance books 40 of attribution immediately and 60 of issuance at effective_at
  • the future 60 does not appear in the current spendable balance
  • two sequential 60-credit purchases against 100 of advance attribute 60 + 40, with only the remaining 20 issued in the future
  • promotional purchase coverage verifies that grant realization and lineage use the same purchase-time ledger transaction group

Invoice-settled purchases keep their existing trigger at invoice draft creation. This PR does not add payment control or recurring subscription grants.

Cancellation / void limitation

The existing void path operates on currently issued future-balance-overage value for the source charge. Before a future grant reaches effective_at, its scheduled remainder has no spendable balance slice to void, and the purchase-time advance attribution is not such a slice. As a result, future-effective grants remain non-voidable before effective_at; this PR does not add cancellation of scheduled ledger entries or claim to solve that lifecycle. Void behavior for remaining issued value after effective_at is unchanged.

Tests

  • nix develop --impure .#ci -c env POSTGRES_HOST=127.0.0.1 go vet -tags=dynamic ./openmeter/ledger/chargeadapter ./openmeter/billing/charges/creditpurchase/service
  • nix develop --impure .#ci -c env POSTGRES_HOST=127.0.0.1 go test -tags=dynamic -v ./openmeter/ledger/chargeadapter ./openmeter/billing/charges/creditpurchase/service
  • nix develop --impure .#ci -c env POSTGRES_HOST=127.0.0.1 go test -tags=dynamic -v ./test/credits

All database-backed tests ran against PostgreSQL; none were skipped.

Summary by CodeRabbit

  • Bug Fixes

    • Improved the timing of credit purchase accounting for purchases with past or future effective dates.
    • Advance credits are now attributed at the appropriate purchase or effective time, preventing over-attribution across multiple future purchases.
    • Credit grant timestamps and ledger transaction references now remain consistent and accurate.
  • Tests

    • Expanded coverage for time-based credit grants, future-effective purchases, balance calculations, and transaction booking dates.

Greptile Summary

This PR changes when credit purchases are recorded in the ledger. The main changes are:

  • Books existing advance attribution when the purchase is initiated.
  • Defers only the remaining credit issuance until the grant becomes effective.
  • Backdates attribution for already-effective purchases to match issuance and settlement.
  • Adds tests for past-effective, future-effective, sequential, and promotional purchases.

Confidence Score: 5/5

This looks safe to merge.

  • Past-effective attribution now uses the same effective time as issuance and settlement.
  • Future-effective purchases attribute existing advances immediately while keeping the remainder deferred.
  • Tests cover the timing cases changed by this update.
  • No blocking issues were found in the changed code.

Important Files Changed

Filename Overview
openmeter/ledger/chargeadapter/creditpurchase.go Separates advance-attribution timing from deferred issuance and keeps past-effective ledger entries aligned.
openmeter/ledger/chargeadapter/creditpurchase_test.go Adds tests for historical timing, future issuance, and sequential consumption of an existing advance.
openmeter/billing/charges/creditpurchase/service/promotional_test.go Verifies promotional grant realization and lineage use the purchase-time ledger transaction group.

Reviews (2): Last reviewed commit: "fix(ledger): book advance attribution at..." | Re-trigger Greptile

Context used:

  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Credit purchase ledger attribution now separates service-period effective time from purchase-time advance attribution. New tests verify past and future effective periods, capped advance allocation, template booking timestamps, balances, promotional grant timestamps, and lineage identifiers.

Changes

Credit purchase timing

Layer / File(s) Summary
Effective-time attribution logic
openmeter/ledger/chargeadapter/creditpurchase.go
Advance attribution uses the service-period effective time when it is past, otherwise the current time; issuance and promotional settlement use the effective time.
Ledger attribution coverage
openmeter/ledger/chargeadapter/creditpurchase_test.go
Tests cover past-effective backdating, future-effective backfill, capped subsequent attribution, template-specific booking times, and timestamped balances.
Promotional timing assertions
openmeter/billing/charges/creditpurchase/service/promotional_test.go
Tests freeze purchase time and assert exact realization timestamps, adapter inputs, transaction groups, and lineage identifiers.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant issueCreditPurchaseGroup
  participant clockNow
  participant LedgerTemplates
  issueCreditPurchaseGroup->>clockNow: read current time for future effective periods
  issueCreditPurchaseGroup->>LedgerTemplates: post advance attribution at selected timestamp
  issueCreditPurchaseGroup->>LedgerTemplates: post issuance and settlement at effectiveAt
Loading

Possibly related PRs

Suggested labels: area/billing

Suggested reviewers: turip, tothandras

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: booking advance attribution at purchase time.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 codex/credit-purchase-attribution-timing

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.

@GAlexIHU GAlexIHU added the release-note/bug-fix Release note: Bug Fixes label Jul 21, 2026
Comment thread openmeter/ledger/chargeadapter/creditpurchase.go Outdated
@GAlexIHU
GAlexIHU force-pushed the codex/credit-purchase-attribution-timing branch from 0e96582 to 5f4e6a8 Compare July 27, 2026 11:26
@GAlexIHU
GAlexIHU marked this pull request as ready for review July 27, 2026 11:26
@GAlexIHU
GAlexIHU requested a review from a team as a code owner July 27, 2026 11:26

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/ledger/chargeadapter/creditpurchase_test.go (1)

205-294: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Freeze the clock in these future-effective credit-purchase tests.

env.Now() reads from clock.Now(), and these tests don’t call clock.FreezeTime before OnCreditPurchaseInitiated; the handler resolves future attribution with clock.Now() for effective-period IssueCustomerReceivable scenarios. Capture a frozen purchase time and make the handler use that purchase time for future-effective attribution, otherwise exact bookedAt and balance boundary checks can become microsecond-flaky.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/chargeadapter/creditpurchase_test.go` around lines 205 -
294, Freeze the test clock at the captured purchase time in both
future-effective tests, before calling OnCreditPurchaseInitiated. Use the test
environment’s clock-freezing helper so handler calls resolve clock.Now() to
purchasedAt, preserving deterministic bookedAt and balance boundary assertions.
🧹 Nitpick comments (1)
openmeter/ledger/chargeadapter/creditpurchase_test.go (1)

1011-1035: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New helper duplicates the query in transactionBookedAtTimes.

transactionBookedAtByTemplateCode re-issues the same Where/Order/All query as the pre-existing transactionBookedAtTimes (lines 987-1009), differing only in how the results are grouped. Consider extracting the shared query into a small private helper both can call, so the two output shapes are derived from one query path.

As per coding guidelines, avoid duplicate test helpers, and add helpers only when reused or when they express non-obvious domain semantics.

♻️ Proposed refactor
+func (e *creditPurchaseHandlerTestEnv) queryTransactionRows(t *testing.T, groupID string) []*db.LedgerTransaction {
+	t.Helper()
+
+	transactionRows, err := e.DB.LedgerTransaction.Query().
+		Where(
+			ledgertransactiondb.Namespace(e.Namespace),
+			ledgertransactiondb.GroupID(groupID),
+		).
+		Order(
+			ledgertransactiondb.ByCreatedAt(),
+			ledgertransactiondb.ByID(),
+		).
+		All(t.Context())
+	require.NoError(t, err)
+	require.NotEmpty(t, transactionRows, "expected at least one ledger transaction for group")
+
+	return transactionRows
+}
+
 func (e *creditPurchaseHandlerTestEnv) transactionBookedAtByTemplateCode(t *testing.T, groupID string) map[string][]time.Time {
 	t.Helper()
-
-	transactionRows, err := e.DB.LedgerTransaction.Query().
-		Where(
-			ledgertransactiondb.Namespace(e.Namespace),
-			ledgertransactiondb.GroupID(groupID),
-		).
-		Order(
-			ledgertransactiondb.ByCreatedAt(),
-			ledgertransactiondb.ByID(),
-		).
-		All(t.Context())
-	require.NoError(t, err)
-	require.NotEmpty(t, transactionRows, "expected at least one ledger transaction for group")
+	transactionRows := e.queryTransactionRows(t, groupID)
 
 	out := make(map[string][]time.Time, len(transactionRows))

(Note: adjust the row type name to match the actual generated ent type.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/chargeadapter/creditpurchase_test.go` around lines 1011 -
1035, Extract the shared ledger transaction query from transactionBookedAtTimes
and transactionBookedAtByTemplateCode into one private helper returning the
queried transaction rows. Update both helpers to reuse it while preserving their
existing output shapes and assertions, including ordering and error handling.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openmeter/ledger/chargeadapter/creditpurchase.go`:
- Around line 303-311: Update the settlement entry built with
SettleCustomerReceivableFromPaymentTemplate so a past-effective promotional
grant uses the recording/materialization timestamp instead of effectiveAt.
Preserve effectiveAt for purchase-time attribution and only alter settlement
timing when the grant is backdated, keeping ledger ordering consistent.

---

Outside diff comments:
In `@openmeter/ledger/chargeadapter/creditpurchase_test.go`:
- Around line 205-294: Freeze the test clock at the captured purchase time in
both future-effective tests, before calling OnCreditPurchaseInitiated. Use the
test environment’s clock-freezing helper so handler calls resolve clock.Now() to
purchasedAt, preserving deterministic bookedAt and balance boundary assertions.

---

Nitpick comments:
In `@openmeter/ledger/chargeadapter/creditpurchase_test.go`:
- Around line 1011-1035: Extract the shared ledger transaction query from
transactionBookedAtTimes and transactionBookedAtByTemplateCode into one private
helper returning the queried transaction rows. Update both helpers to reuse it
while preserving their existing output shapes and assertions, including ordering
and error handling.
🪄 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 Plus

Run ID: 900bf32c-a5ef-40bd-a9ad-9cb71d085ca4

📥 Commits

Reviewing files that changed from the base of the PR and between 1210e1a and 5f4e6a8.

📒 Files selected for processing (3)
  • openmeter/billing/charges/creditpurchase/service/promotional_test.go
  • openmeter/ledger/chargeadapter/creditpurchase.go
  • openmeter/ledger/chargeadapter/creditpurchase_test.go

Comment thread openmeter/ledger/chargeadapter/creditpurchase.go
Comment thread openmeter/ledger/chargeadapter/creditpurchase.go
@GAlexIHU
GAlexIHU merged commit c3c4233 into main Jul 29, 2026
27 of 28 checks passed
@GAlexIHU
GAlexIHU deleted the codex/credit-purchase-attribution-timing branch July 29, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/bug-fix Release note: Bug Fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants