Skip to content

fix: fix late review comments for costbasis change - #4883

Merged
turip merged 2 commits into
mainfrom
feat/fix-costbasis-late-review-comments
Aug 7, 2026
Merged

turip merged 2 commits into
mainfrom
feat/fix-costbasis-late-review-comments

Conversation

@turip

@turip turip commented Aug 7, 2026 •

Copy link
Copy Markdown
Member

Summary

  • correct the credit-purchase cost-basis foreign-key delete behavior and enforce one-to-one cost-basis relationships
  • restore the original credit-purchase effective-at behavior
  • add JSON round-trip support for credit-purchase cost-basis variants
  • consolidate legacy cost-basis projection, fiat amount calculation, and upgraded edge reuse
  • add regression coverage for the corrected behavior

Root cause

Late review identified that the credit-purchase cost-basis foreign key cascaded in the wrong direction once the relationship became active. The original cost-basis change also unintentionally made service-period start affect credit-purchase effective time and left several representation and persistence invariants enforced only by application conventions.

Impact

Cost-basis records can no longer cascade-delete financial charge records, every charge type enforces the intended one-to-one cost-basis relationship in PostgreSQL, and the follow-up cleanup removes duplicated conversion and loading paths without changing purchase amount behavior.

Validation

  • go test ./openmeter/billing/charges/creditpurchase ./openmeter/billing/charges/creditpurchase/adapter ./openmeter/billing/charges/creditpurchase/service/... ./api/v3/handlers/customers/credits
  • POSTGRES_HOST=127.0.0.1 go test -tags=dynamic ./openmeter/billing/charges/service
  • make migrate-check-lint
  • make migrate-check-validate
  • make lint-go-fast

Ticket: OM-436

Summary by CodeRabbit

  • New Features

    • Added support for reliably handling promotional, fiat, and custom-currency credit purchase cost bases.
    • Improved cost-basis data exchange and compatibility with existing records.
  • Bug Fixes

    • Improved fiat amount calculations and rounding across invoices and external payments.
    • Prevented cost-basis records from being deleted while still referenced.
    • Corrected effective and expiration time validation for credit purchases.
  • Data Integrity

    • Added safeguards to prevent conflicting currency information and duplicate cost-basis associations.

Greptile Summary

The PR corrects credit-purchase cost-basis persistence and effective-time behavior while consolidating conversion logic.

  • Changes the credit-purchase cost-basis foreign key from cascading deletion to restricted deletion.
  • Adds unique cost-basis reference indexes for credit-purchase, flat-fee, and usage-based charges.
  • Adds validated JSON round-trip support for fiat and custom-currency cost-basis variants.
  • Reuses upgraded cost-basis edges and centralizes fiat amount calculation.
  • Restores creation-time effective-date behavior and adds regression coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
openmeter/billing/charges/creditpurchase/adapter/costbasis.go Consolidates legacy cost-basis projection and preserves the materialized edge through schema upgrades.
openmeter/billing/charges/creditpurchase/adapter/charge.go Reuses the row-locked, eagerly loaded cost-basis edge after charge updates and voiding.
openmeter/billing/charges/creditpurchase/charge.go Restores creation-time effective-date semantics and limits intrinsic expiry comparison to explicit effective times.
openmeter/billing/charges/creditpurchase/service.go Enforces creation-time expiry validation when the effective time is implicit.
openmeter/billing/charges/creditpurchase/costbasis.go Adds validated JSON serialization for all cost-basis variants and centralizes rounded fiat conversion.
openmeter/ent/schema/chargescreditpurchase.go Restricts deletion of referenced cost-basis records and enforces one-to-one references.
tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql Applies the corrected foreign-key behavior and unique cost-basis indexes to PostgreSQL.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Input[Credit-purchase input] --> Validate[Validate intent and expiry]
  Validate --> Normalize[Normalize purchase intent]
  Normalize --> Persist[Persist charge]
  Persist --> CostBasis{Cost-basis schema}
  CostBasis -->|Legacy| Upgrade[Materialize dedicated cost basis]
  CostBasis -->|Current| Reuse[Reuse loaded cost-basis edge]
  Upgrade --> Reuse
  Reuse --> Resolve[Resolve fiat rate]
  Resolve --> Amount[Calculate rounded fiat amount]
  Amount --> Invoice[Invoice or external settlement]
Loading

Reviews (3): Last reviewed commit: "fix: validate credit purchase expiry at ..." | Re-trigger Greptile

Context used:

@turip
turip requested a review from a team as a code owner August 7, 2026 14:58
@turip turip added release-note/misc Miscellaneous changes area/billing labels Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Credit purchase cost bases now support JSON round trips and shared fiat conversion. Legacy settlements use centralized validation and mapping. Database relationships restrict referenced cost-basis deletion. Effective-time and expiration validation now handle missing explicit effective times.

Changes

Credit purchase cost-basis flow

Layer / File(s) Summary
Cost-basis representation and valuation
openmeter/billing/charges/creditpurchase/costbasis.go, openmeter/billing/charges/creditpurchase/costbasis_test.go
CostBasis now supports validated JSON encoding and decoding. ResolvedCostBasis.FiatAmount centralizes fiat conversion and rounding.
Legacy settlement mapping and charge updates
openmeter/billing/charges/creditpurchase/adapter/costbasis.go, openmeter/billing/charges/creditpurchase/adapter/mapper.go, openmeter/billing/charges/creditpurchase/adapter/charge.go
Legacy promotional, fiat, and custom-currency settlements use shared validation and mapping. Charge updates reuse the upgraded cost-basis edge.
Cost-basis persistence constraints
openmeter/ent/schema/chargescreditpurchase.go, openmeter/ent/schema/chargesflatfee.go, openmeter/ent/schema/chargesusagebased.go, tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.*.sql, openmeter/billing/charges/service/creditpurchase_test.go
Charge schemas and migrations add unique cost-basis indexes and restrict deletion of referenced credit-purchase cost bases.
Valuation consumers and effective-time validation
openmeter/billing/charges/creditpurchase/service/*.go, openmeter/billing/charges/creditpurchase/service/realizations/service.go, api/v3/handlers/customers/credits/convert.go, openmeter/billing/charges/creditpurchase/charge.go, openmeter/billing/charges/creditpurchase/service.go, openmeter/billing/charges/creditpurchase/charge_test.go
Invoice, external payment, and API conversion paths use FiatAmount. Effective-time calculation no longer backdates to the service period, and expiration validation covers missing explicit effective times.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CreditPurchase
  participant ResolvedCostBasis
  participant InvoiceAndPaymentServices
  participant CustomerCreditsAPI
  CreditPurchase->>ResolvedCostBasis: resolve fiat amount
  ResolvedCostBasis-->>InvoiceAndPaymentServices: rounded FiatAmount
  InvoiceAndPaymentServices-->>CustomerCreditsAPI: invoice or external purchase amount
Loading

Possibly related PRs

Suggested labels: release-note/bug-fix, area/billing

Suggested reviewers: tothandras

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title refers to the cost-basis changes, but it is broad and emphasizes review comments rather than the specific behavior being fixed.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fix-costbasis-late-review-comments

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.

@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: 2

🧹 Nitpick comments (2)
openmeter/billing/charges/creditpurchase/costbasis_test.go (1)

62-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nice round-trip table. Consider a few negative decode cases too.

The happy paths are covered well. UnmarshalJSON also adds several rejection branches that no test exercises yet: a fiat payload that carries custom-currency fields, a dynamic payload that carries rate or currencyCostBasisID, a pinned payload without an ID, and an unknown mode. A small table of raw JSON strings mapped to expected error substrings would lock that behavior in.

Not a blocker, just a cheap way to keep the strict validation honest during future refactors.

🤖 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/billing/charges/creditpurchase/costbasis_test.go` around lines 62 -
99, Extend TestCostBasisJSONRoundTrip with table-driven negative JSON cases
covering fiat payloads with custom-currency fields, dynamic payloads with rate
or currencyCostBasisID, pinned payloads without an ID, and an unknown mode.
Unmarshal each raw payload into CostBasis and assert the error contains the
expected validation substring, preserving the existing happy-path round-trip
assertions.
tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql (1)

1-11: 🩺 Stability & Availability | 🔵 Trivial

Use non-blocking migration DDL for the index/foreign-key changes.

Both directions touch live charge tables with DDL that acquires locks while holding the migration transaction. The up migration only marks its schema migrations as locked (atlas:nolint MF101), so add an online rollout plan or a maintenance window for both up.sql and down.sql, using patterns such as non-blocking indexes/constraint validation where the chosen runner supports them.

🤖 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
`@tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql`
around lines 1 - 11, The charge-table index and foreign-key changes use blocking
DDL in both migration directions. Update
tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql
lines 1-11 and
tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.down.sql
lines 1-8 to use the runner’s supported online index creation and
constraint-validation patterns, or explicitly schedule these operations for a
maintenance window; ensure both up and down migrations have a non-blocking
rollout plan rather than relying only on atlas:nolint MF101.

Sources: Path instructions, Linters/SAST tools

🤖 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/billing/charges/service/creditpurchase_test.go`:
- Around line 319-337: The “referenced cost basis cannot be deleted” subtest
currently accepts any deletion error and does not verify the referenced row
survives. Update the assertions after DeleteOneID to require the specific
charge_credit_purchase_cost_basis_charge_fk foreign-key violation, then fetch
and verify the cost-basis row remains readable, while preserving the existing
persisted-charge checks.

In
`@tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql`:
- Around line 1-11: Add a preflight before the unique index statements in the
migration that detects duplicate non-NULL cost_basis_id values in
charge_flat_fees, charge_usage_based, and charge_credit_purchases. Make the
migration fail before creating any indexes when duplicates exist, or explicitly
clean up duplicates first while preserving valid references.

---

Nitpick comments:
In `@openmeter/billing/charges/creditpurchase/costbasis_test.go`:
- Around line 62-99: Extend TestCostBasisJSONRoundTrip with table-driven
negative JSON cases covering fiat payloads with custom-currency fields, dynamic
payloads with rate or currencyCostBasisID, pinned payloads without an ID, and an
unknown mode. Unmarshal each raw payload into CostBasis and assert the error
contains the expected validation substring, preserving the existing happy-path
round-trip assertions.

In
`@tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql`:
- Around line 1-11: The charge-table index and foreign-key changes use blocking
DDL in both migration directions. Update
tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql
lines 1-11 and
tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.down.sql
lines 1-8 to use the runner’s supported online index creation and
constraint-validation patterns, or explicitly schedule these operations for a
maintenance window; ensure both up and down migrations have a non-blocking
rollout plan rather than relying only on atlas:nolint MF101.
🪄 Autofix

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: d695625c-9ce6-4fb1-8d2d-e1420881ebf2

📥 Commits

Reviewing files that changed from the base of the PR and between 2ecb2f7 and e4975ae.

⛔ Files ignored due to path filters (2)
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (17)
  • api/v3/handlers/customers/credits/convert.go
  • openmeter/billing/charges/creditpurchase/adapter/charge.go
  • openmeter/billing/charges/creditpurchase/adapter/costbasis.go
  • openmeter/billing/charges/creditpurchase/adapter/mapper.go
  • openmeter/billing/charges/creditpurchase/charge.go
  • openmeter/billing/charges/creditpurchase/charge_test.go
  • openmeter/billing/charges/creditpurchase/costbasis.go
  • openmeter/billing/charges/creditpurchase/costbasis_test.go
  • openmeter/billing/charges/creditpurchase/service/create.go
  • openmeter/billing/charges/creditpurchase/service/lineengine.go
  • openmeter/billing/charges/creditpurchase/service/realizations/service.go
  • openmeter/billing/charges/service/creditpurchase_test.go
  • openmeter/ent/schema/chargescreditpurchase.go
  • openmeter/ent/schema/chargesflatfee.go
  • openmeter/ent/schema/chargesusagebased.go
  • tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.down.sql
  • tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql

Comment thread openmeter/billing/charges/service/creditpurchase_test.go
@turip
turip force-pushed the feat/fix-costbasis-late-review-comments branch from e4975ae to 344a252 Compare August 7, 2026 15:25

@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

🧹 Nitpick comments (2)
tools/migrate/credit_purchase_cost_basis_backfill_test.go (2)

270-298: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider mirroring the cost_basis_id unique index in the test DDL.

The follow-up migration tools/migrate/migrations/20260807145338_enforce_charge_cost_basis_relationships.up.sql adds CREATE UNIQUE INDEX "chargecreditpurchases_cost_basis_id" ON "charge_credit_purchases" ("cost_basis_id"). The test schema here omits it, so a backfill regression that assigned one cost_basis_id to two charges would still pass. The temp table in the migration already declares cost_basis_id as UNIQUE, so this is belt-and-braces, but it is a one-liner and it pins the invariant the two migrations agree on.

Note that a plain unique index tolerates multiple NULL values in PostgreSQL, so the promotional and fiat rows stay valid.

🧪 Proposed test DDL addition
 			initial_payment_settlement_status text NULL,
 			cost_basis_id text NULL
 		);
+
+		CREATE UNIQUE INDEX chargecreditpurchases_cost_basis_id
+			ON charge_credit_purchases (cost_basis_id);
 
 		CREATE TABLE charge_credit_purchase_cost_bases (
🤖 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 `@tools/migrate/credit_purchase_cost_basis_backfill_test.go` around lines 270 -
298, Update the test DDL for charge_credit_purchases to add the unique index
chargecreditpurchases_cost_basis_id on cost_basis_id, matching the follow-up
migration and preserving PostgreSQL’s handling of multiple NULL values.

236-247: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Each subtest starts a fresh PostgreSQL database.

withCreditPurchaseCostBasisBackfillTables calls testutils.InitPostgresDB per case, so this table of 12 cases pays 12 database initializations. Every case expects the migration to fail, and the migration wraps everything in BEGIN; ... COMMIT;, so a failed run leaves no rows behind. You could share one database across the cases and delete the inserted row between them. Totally optional, just a CI-time win.

🤖 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 `@tools/migrate/credit_purchase_cost_basis_backfill_test.go` around lines 236 -
247, Optimize the table-driven test around
withCreditPurchaseCostBasisBackfillTables by initializing the PostgreSQL
database once for all cases instead of once per subtest. Reuse that database
while ensuring each case removes its inserted row before the next case,
preserving the existing migration failure assertions and isolation.
🤖 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/billing/charges/creditpurchase/charge_test.go`:
- Around line 88-124: Add a test case alongside
TestCreateInputValidateRejectsHistoricalExpiryWithoutEffectiveAt where ExpiresAt
equals the frozen now value, and assert validation rejects it with the same
effective-time error. Keep the existing earlier-expiry coverage and reuse the
current CreateInput setup.

---

Nitpick comments:
In `@tools/migrate/credit_purchase_cost_basis_backfill_test.go`:
- Around line 270-298: Update the test DDL for charge_credit_purchases to add
the unique index chargecreditpurchases_cost_basis_id on cost_basis_id, matching
the follow-up migration and preserving PostgreSQL’s handling of multiple NULL
values.
- Around line 236-247: Optimize the table-driven test around
withCreditPurchaseCostBasisBackfillTables by initializing the PostgreSQL
database once for all cases instead of once per subtest. Reuse that database
while ensuring each case removes its inserted row before the next case,
preserving the existing migration failure assertions and isolation.
🪄 Autofix

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: 63fbd000-e57c-46e8-8a8e-78f7ca6a9e97

📥 Commits

Reviewing files that changed from the base of the PR and between e4975ae and 344a252.

⛔ Files ignored due to path filters (1)
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (6)
  • openmeter/billing/charges/creditpurchase/charge.go
  • openmeter/billing/charges/creditpurchase/charge_test.go
  • openmeter/billing/charges/creditpurchase/service.go
  • tools/migrate/credit_purchase_cost_basis_backfill_test.go
  • tools/migrate/migrations/20260807061955_migrate_credit_purchase_cost_basis_schema_level_2.down.sql
  • tools/migrate/migrations/20260807061955_migrate_credit_purchase_cost_basis_schema_level_2.up.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/billing/charges/creditpurchase/charge.go

Comment thread openmeter/billing/charges/creditpurchase/charge_test.go Outdated
@turip
turip force-pushed the feat/fix-costbasis-late-review-comments branch from 344a252 to 10fb5c7 Compare August 7, 2026 15:42
@turip

turip commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Regarding CodeRabbit’s blocking-DDL note: thanks for flagging the availability tradeoff. We’re intentionally keeping this migration transactional and accepting the write-lock behavior for this rollout, which will be coordinated accordingly. Splitting it into non-transactional concurrent-index and staged constraint migrations would introduce partial-application and recovery complexity that we do not want to take on in this PR. No migration change is planned for this item.

@turip
turip enabled auto-merge (squash) August 7, 2026 15:45
@turip
turip merged commit 0fe0ebf into main Aug 7, 2026
27 checks passed
@turip
turip deleted the feat/fix-costbasis-late-review-comments branch August 7, 2026 15:49
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