Skip to content

refactor(billing): use fiat currency for invoices#4774

Merged
turip merged 1 commit into
mainfrom
feat/billing-fiat-currency-fields
Jul 23, 2026
Merged

refactor(billing): use fiat currency for invoices#4774
turip merged 1 commit into
mainfrom
feat/billing-fiat-currency-fields

Conversation

@turip

@turip turip commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

  • model Gathering and Standard invoice currencies as currencyx.FiatCode across billing domain objects and persistence
  • derive the invoice fiat currency for custom-currency charges from their cost-basis intent
  • convert FiatCode to a calculator only at calculation boundaries and regenerate Ent code

Why

Gathering and Standard invoices are settled in fiat currency. Using the broader currencyx.Code type allowed custom-currency codes to flow into invoice persistence and calculation even though those paths require fiat semantics.

Impact

Invoice and invoice-line callers now get compile-time enforcement that the persisted currency is fiat. Custom-currency charges continue to invoice in the fiat currency selected by their cost basis. The physical database columns remain varchar(3), so no SQL migration is required.

Validation

  • make lint-go-fast
  • make test-nocache — all code-related tests passed; the existing notification integration test could not connect to the local Svix service at 127.0.0.1:8071
  • focused flat-fee, usage-based, and credit-purchase charge service tests
  • git diff --check

Greptile Summary

Refactors billing invoices and invoice lines to carry fiat-only currency codes.

  • Derives invoice currency for custom-currency flat-fee and usage-based charges from their cost-basis intent.
  • Converts fiat codes into calculators only at calculation and Stripe boundaries.
  • Updates Ent schema Go types and regenerated persistence code without changing physical varchar columns.
  • Updates billing, Stripe, notification, and integration tests for the new currency type.

Confidence Score: 3/5

This PR should not merge until custom-currency usage totals are converted with their resolved cost basis before being represented as fiat invoice amounts.

Usage rating retains the custom-currency price and calculator, while the changed gathering line carries a fiat code and the resulting custom-unit totals flow into the standard invoice without rate conversion.

openmeter/billing/charges/usagebased/service/create.go, openmeter/billing/charges/usagebased/rating.go, openmeter/billing/charges/usagebased/service/linemapper.go

Important Files Changed

Filename Overview
openmeter/billing/charges/usagebased/service/create.go Assigns the cost-basis fiat currency to custom-currency gathering lines without converting their price.
openmeter/billing/charges/usagebased/rating.go Rating still computes custom-currency charge totals with the original custom price and calculator.
openmeter/billing/charges/usagebased/service/linemapper.go Maps custom-unit realization totals onto fiat-labeled standard lines without applying the cost-basis rate.
openmeter/billing/charges/models/costbasis/intent.go Adds a common accessor for the fiat currency represented by each cost-basis intent mode.
openmeter/ent/schema/billing.go Changes billing currency Go types to FiatCode while retaining the existing varchar(3) storage schema.
openmeter/app/stripe/calculator.go Narrows Stripe calculator construction to fiat codes and preserves calculator behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Custom-currency usage charge] --> B[Cost-basis intent]
  B --> C[Fiat invoice currency]
  A --> D[Rating with custom price and calculator]
  C --> E[Gathering invoice]
  D --> F[Realization run totals]
  E --> G[Standard invoice line]
  F --> G
Loading

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
openmeter/billing/charges/usagebased/service/create.go:142
**Cost basis rate is ignored**

When a usage-based charge is priced in a custom currency with a non-unit fiat cost basis, this assigns the fiat code to the gathering line while rating retains the original custom-currency price and calculator. The resulting custom-unit totals are copied to the fiat invoice without applying the resolved rate, causing an amount such as 100 custom units at 0.5 USD per unit to be invoiced as USD 100 instead of USD 50.

Reviews (1): Last reviewed commit: "refactor(billing): use fiat currency for..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used (3)

Summary by CodeRabbit

  • Improvements
    • Billing and invoicing now consistently handle fiat currencies across invoices, invoice lines, charges, payments, and Stripe integrations.
    • Invoice currencies derived from custom pricing or cost-basis settings are resolved more reliably.
    • Non-fiat currencies are rejected where invoice currency is required.
  • Tests
    • Expanded coverage verifies correct currency assignment and invoice grouping across billing scenarios.

Comment thread openmeter/billing/charges/usagebased/service/create.go
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR narrows billing and Stripe currency contracts from currencyx.Code to currencyx.FiatCode, updates currency resolution and grouping paths, adds charge invoice-currency helpers, and adjusts affected tests and persistence schemas.

Changes

Fiat currency contracts

Layer / File(s) Summary
Billing and invoice data shapes
openmeter/billing/gatheringinvoice.go, openmeter/billing/stdinvoice.go, openmeter/billing/stdinvoiceline.go, openmeter/billing/invoiceline*.go
Currency fields, accessors, filters, and grouping maps now use currencyx.FiatCode.
HTTP, Stripe, sequence, and persistence contracts
openmeter/app/stripe/*, openmeter/billing/httpdriver/*, openmeter/billing/sequence/*, openmeter/ent/schema/billing.go
Currency inputs and stored schema fields are narrowed to fiat codes while retaining existing validation and JSON fields.

Charge invoice-currency resolution

Layer / File(s) Summary
Cost-basis and charge resolution
openmeter/billing/charges/models/costbasis/intent.go, openmeter/billing/charges/{flatfee,usagebased}/charge.go
Charges resolve invoice currencies directly for fiat charges or through cost-basis fiat currencies for custom charges.
Gathering-line and calculator paths
openmeter/billing/charges/{flatfee,usagebased,creditpurchase}/service/*, openmeter/billing/charges/service/*
Charge, invoice, pending-line, and calculator paths use AsFiatCurrency() and store the resolved fiat code.

Currency grouping and validation

Layer / File(s) Summary
Invoice provisioning and synchronization
openmeter/billing/service/*, openmeter/billing/charges/invoiceupdater/*, openmeter/billing/worker/subscriptionsync/*
Currency-keyed maps and grouping callbacks now use currencyx.FiatCode; target-state lines reject non-fiat currencies.
Test and fixture updates
openmeter/billing/**/*_test.go, test/**/*_test.go, test/billing/suite.go
Affected inputs, expected values, filters, and gathering-line assertions now construct fiat currency codes.

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

Possibly related PRs

Suggested labels: kind/refactor, release-note/misc

Suggested reviewers: tothandras, chrisgacsal

🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: billing invoices are being refactored to use fiat currency types.
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 feat/billing-fiat-currency-fields

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.

@turip turip added release-note/ignore Ignore this change when generating release notes area/billing labels Jul 23, 2026
@turip
turip marked this pull request as ready for review July 23, 2026 08:41
@turip
turip requested a review from a team as a code owner July 23, 2026 08:41

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

Caution

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

⚠️ Outside diff range comments (1)
openmeter/billing/charges/usagebased/service/create.go (1)

121-142: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Convert custom-currency amounts before fiat invoice-line construction.

Both builders switch the line currency to the resolved fiat code without converting the existing amount from the source charge currency. That can produce mispriced invoices for custom-currency charges.

  • openmeter/billing/charges/usagebased/service/create.go#L121-L142: convert intent.Price using the cost-basis rate before assigning invoiceCurrency.
  • openmeter/billing/charges/flatfee/service/create.go#L198-L225: convert amountAfterProration before constructing Price with invoiceCurrency.
🤖 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/usagebased/service/create.go` around lines 121 -
142, Convert custom-currency amounts to the resolved fiat currency using the
cost-basis rate before constructing invoice lines: in
openmeter/billing/charges/usagebased/service/create.go:121-142, convert
intent.Price before assigning invoiceCurrency in the GatheringLine; in
openmeter/billing/charges/flatfee/service/create.go:198-225, convert
amountAfterProration before constructing Price with invoiceCurrency. Preserve
existing pricing and proration behavior apart from the required currency
conversion.
🤖 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/models/costbasis/intent.go`:
- Around line 133-160: Update GetFiatCurrency to validate the FiatCurrency
pointer returned by the ModeDynamic, ModePinned, and ModeManual intent variants
before returning success; return an appropriate validation error when it is nil,
while preserving existing conversion errors and valid non-nil results.

In `@openmeter/billing/charges/service/usagebased_costbasis_test.go`:
- Around line 143-144: Extend the assertions in the usage-based cost-basis tests
around GatheringLineToCreate to verify the converted usage amounts, not only the
USD currency. Cover custom-currency cases using the resolved manual or pinned
cost-basis rate, and define the expected amount for the dynamic-rate case so the
generated fiat lines validate the conversion.

In `@openmeter/billing/charges/usagebased/service/linemapper.go`:
- Around line 134-136: Update the line-mapping flow around AsFiatCurrency to
apply the custom-currency conversion rate to detailed amounts, credits, and
expected totals before WithCreditsApplied and RoundToPrecision; otherwise reject
custom-currency input instead of emitting incorrectly labeled fiat amounts.

---

Outside diff comments:
In `@openmeter/billing/charges/usagebased/service/create.go`:
- Around line 121-142: Convert custom-currency amounts to the resolved fiat
currency using the cost-basis rate before constructing invoice lines: in
openmeter/billing/charges/usagebased/service/create.go:121-142, convert
intent.Price before assigning invoiceCurrency in the GatheringLine; in
openmeter/billing/charges/flatfee/service/create.go:198-225, convert
amountAfterProration before constructing Price with invoiceCurrency. Preserve
existing pricing and proration behavior apart from the required currency
conversion.
🪄 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: c0eed596-327e-4776-ae8b-ea4f11941f20

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5a848 and 547e4d2.

⛔ Files ignored due to path filters (14)
  • openmeter/ent/db/billinggatheringinvoiceline.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinggatheringinvoiceline/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinggatheringinvoiceline_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoice.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoice/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoice_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoicesplitlinegroup.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoicesplitlinegroup/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoicesplitlinegroup_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entmixinaccessor.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
📒 Files selected for processing (61)
  • openmeter/app/stripe/calculator.go
  • openmeter/app/stripe/client/invoice.go
  • openmeter/billing/charges/creditpurchase/service/create.go
  • openmeter/billing/charges/flatfee/charge.go
  • openmeter/billing/charges/flatfee/service/create.go
  • openmeter/billing/charges/flatfee/service/linemapper.go
  • openmeter/billing/charges/flatfee/service/manualedit.go
  • openmeter/billing/charges/invoiceupdater/invoiceupdate.go
  • openmeter/billing/charges/models/costbasis/intent.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/creditpurchase_test.go
  • openmeter/billing/charges/service/flatfee_costbasis_test.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/billing/charges/service/invoice.go
  • openmeter/billing/charges/service/pendinglines.go
  • openmeter/billing/charges/service/pendinglines_test.go
  • openmeter/billing/charges/service/taxcode_test.go
  • openmeter/billing/charges/service/usagebased_costbasis_test.go
  • openmeter/billing/charges/usagebased/charge.go
  • openmeter/billing/charges/usagebased/service/create.go
  • openmeter/billing/charges/usagebased/service/lineengine_test.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/charges/usagebased/service/linemapper_test.go
  • openmeter/billing/charges/usagebased/service/run/payment_test.go
  • openmeter/billing/gatheringinvoice.go
  • openmeter/billing/httpdriver/invoice.go
  • openmeter/billing/httpdriver/invoice_test.go
  • openmeter/billing/httpdriver/invoiceline.go
  • openmeter/billing/invoiceline.go
  • openmeter/billing/invoicelinesplitgroup.go
  • openmeter/billing/sequence/sequence.go
  • openmeter/billing/sequence/service/service.go
  • openmeter/billing/sequence/service/service_test.go
  • openmeter/billing/service/gatheringinvoicependinglines.go
  • openmeter/billing/service/invoicecalc/collectionat_test.go
  • openmeter/billing/service/invoiceupdate.go
  • openmeter/billing/service/invoiceupdate_test.go
  • openmeter/billing/service/stdinvoiceline.go
  • openmeter/billing/stdinvoice.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/stdinvoiceline_test.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/invoiceupdater/invoiceupdate.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/ent/schema/billing.go
  • openmeter/notification/internal/rule.go
  • test/app/custominvoicing/invocing_test.go
  • test/app/stripe/invoice_credits_test.go
  • test/app/stripe/invoice_test.go
  • test/billing/adapter_test.go
  • test/billing/collection_test.go
  • test/billing/discount_test.go
  • test/billing/invoice_test.go
  • test/billing/lineengine_test.go
  • test/billing/schemamigration_test.go
  • test/billing/suite.go
  • test/billing/tax_test.go
  • test/billing/taxcode_dual_write_test.go
  • test/billing/unitconfig_legacy_test.go
  • test/credits/credit_then_invoice_test.go
  • test/credits/sanity_test.go
  • test/customer/subject.go

Comment on lines +133 to +160
// GetFiatCurrency returns the fiat currency in which the custom-currency cost
// basis is expressed, regardless of the selected resolution mode.
func (i Intent) GetFiatCurrency() (*currencyx.FiatCurrency, error) {
switch i.kind {
case ModeDynamic:
intent, err := i.AsDynamic()
if err != nil {
return nil, err
}

return intent.FiatCurrency, nil
case ModePinned:
intent, err := i.AsPinned()
if err != nil {
return nil, err
}

return intent.FiatCurrency, nil
case ModeManual:
intent, err := i.AsManual()
if err != nil {
return nil, err
}

return intent.FiatCurrency, nil
default:
return nil, models.NewGenericValidationError(fmt.Errorf("invalid intent kind: %s", i.kind))
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching intent/costbasis:"
git ls-files | rg '(^|/)openmeter/billing/charges/models/costbasis/intent\.go$|costbasis' || true

echo
echo "Relevant intent outline:"
ast-grep outline openmeter/billing/charges/models/costbasis/intent.go --view expanded || true

echo
echo "Interest locations in intent.go:"
sed -n '1,240p' openmeter/billing/charges/models/costbasis/intent.go

echo
echo "Search GetFiatCurrency/getFiatCode/get invoicesCurrency:"
rg -n "GetFiatCurrency|GetFiatCode|GetInvoiceCurrency|fiatCurrency|FiatCurrency" openmeter/billing/charges -S

Repository: openmeterio/openmeter

Length of output: 23791


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search GetFiatCurrency/getFiatCode/get InvoiceCurrency:"
rg -n "GetFiatCurrency|GetFiatCode|GetInvoiceCurrency|fiatCurrency|FiatCurrency" openmeter/billing/charges -S

echo
echo "Search all repository usages:"
rg -n "GetFiatCurrency|GetFiatCode|GetInvoiceCurrency" . -S

Repository: openmeterio/openmeter

Length of output: 18434


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "costbasis model relevant sections:"
sed -n '1,280p' openmeter/billing/charges/models/costbasis/model.go

echo
echo "currencyx fiat validation relevant sections:"
sed -n '1,260p' pkg/currencyx/costbasis.go
sed -n '150,210p' pkg/currencyx/currency.go

echo
echo "flatfee charge invoice path:"
sed -n '75,110p' openmeter/billing/charges/flatfee/charge.go

echo
echo "usagebased charge invoice path:"
sed -n '80,110p' openmeter/billing/charges/usagebased/charge.go

echo
echo "read-only nil-pointer behavior probe:"
cat > /tmp/probe.go <<'GO'
package main

type FiatCode string
type FiatCurrency struct{ code FiatCode }

func (f *FiatCurrency) GetFiatCode() FiatCode { return f.code }

type Mode string
type DynamicIntent struct{ FiatCurrency *FiatCurrency }
type PinnedIntent struct{ FiatCurrency *FiatCurrency }
type ManualIntent struct{ FiatCurrency *FiatCurrency }
type Intent struct{ kind Mode; dynamic *DynamicIntent; pinned *PinnedIntent; manual *ManualIntent }

func (i Intent) AsDynamic() (DynamicIntent, error) {
	if i.dynamic == nil { return DynamicIntent{}, nil }
	return *i.dynamic, nil
}
func (i Intent) AsPinned() (PinnedIntent, error) {
	if i.pinned == nil { return PinnedIntent{}, nil }
	return *i.pinned, nil
}
func (i Intent) AsManual() (ManualIntent, error) {
	if i.manual == nil { return ManualIntent{}, nil }
	return *i.manual, nil
}
func (i Intent) GetFiatCurrency() (*FiatCurrency, error) {
	switch i.kind {
	case "dynamic":
		intent, err := i.AsDynamic()
		if err != nil { return nil, err }
		return intent.FiatCurrency, nil
	case "pinned":
		intent, err := i.AsPinned()
		if err != nil { return nil, err }
		return intent.FiatCurrency, nil
	case "manual":
		intent, err := i.AsManual()
		if err != nil { return nil, err }
		return intent.FiatCurrency, nil
	default:
		return nil, nil
	}
}
func GetInvoiceCurrency(i Intent) (FiatCode, string) {
	costBasisIntent, err := i.GetFiatCurrency()
	if err != nil { return "", "error" }
	return costBasisIntent.GetFiatCode(), "ok"
}
func main() {
	code, status := GetInvoiceCurrency(Intent{kind: "dynamic"})
	println("code=", string(code), "status=", status)
	_ = GetInvoiceCurrency(Intent{kind: "dynamic"}) // this could panic
}
GO
run_go="$(command -v go || true)"
if [ -n "$run_go" ]; then
  go test /tmp/probe.go
else
  cat /tmp/probe.go
fi

Repository: openmeterio/openmeter

Length of output: 11902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Intent currency accessor implementations:"
rg -n "func \\(i Intent\\) (GetCurrency|GetCostBasisIntent|GetCode|FiatCurrency|CostBasisIntent)|AsCurrency|costBasisIntent" openmeter/billing/charges/models/costbasis openmeter/billing/charges/currencies -S || true

echo
echo "Models/costbasis remaining outline:"
ast-grep outline openmeter/billing/charges/models/costbasis/*.go --view expanded || true

echo
echo "CostBasisIntent definition usages:"
rg -n "type CostBasisIntent|CostBasisIntent" openmeter/billing/charges/models openmeter/billing/charges/currencies -S || true

echo
echo "Read-only nil-pointer behavior probe:"
cat > /tmp/probe.go <<'GO'
package main

import (
	"errors"
	"fmt"
)

type FiatCode string
type FiatCurrency struct{ code FiatCode }
type Mode string

func main() {
	var ptr *FiatCurrency
	fmt.Printf("value %#v error=%v panic=%v\n", nil)
	_, err := fmt.Sprint(ptr)
	fmt.Println("print err:", err != nil && errors.Is(err, (*runtime.Error)(nil).Error()) || err != nil)
}
GO
run_go="$(command -v go || true)"
if [ -n "$run_go" ]; then
  go build -o /tmp/probe /tmp/probe.go && /tmp/probe
else
  go doc fmt.Sprint
fi

Repository: openmeterio/openmeter

Length of output: 5354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Intent methods/usages beyond current files:"
rg -n "\.GetFiatCurrency\(|GetFiatCurrency\(|FiatCurrency\." openmeter -S --glob '*.go'
echo
rg -n "type Intent|func \(i Intent\)|func \(c ChargeBase\)" openmeter -S --glob 'openmeter/billing/charges/**/*.go' || true

echo
echo "ChargeBase methods in relevant charge files:"
sed -n '1,130p' openmeter/billing/charges/flatfee/charge.go
echo "----"
sed -n '1,130p' openmeter/billing/charges/usagebased/charge.go

Repository: openmeterio/openmeter

Length of output: 14491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Cost model fiat code schema:"
rg -n "FiatCurrency|fiatCurrency|FiatCurrencyValidator" openmeter/ent openmeter/billing/charges -S --glob '*.go' | head -n 220

echo
echo "Schema files:"
for f in $(rg -l "FiatCurrency" openmeter/ent/schema -S --glob '*.go'); do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

echo
echo "Read-only nil validation behavior probe:"
cat > /tmp/probe.go <<'GO'
package main

import (
	"errors"
	"fmt"
)

func Validate(f interface{}) error {
	if f == nil {
		return errors.New("fiat currency is not initialized")
	}
	return nil
}

func main() {
	var ptr *struct{}
	if err := Validate(ptr); err != nil {
		fmt.Println("nil Validate result:", err)
	}
	fmt.Println("done")
}
GO
run_go="$(command -v go || true)"
if [ -n "$run_go" ]; then
  go run /tmp/probe.go || true
else
  cat /tmp/probe.go
fi

Repository: openmeterio/openmeter

Length of output: 28755


Guard the fiat-c currency pointer before dereferencing it.

GetFiatCurrency() can return (nil, nil) for malformed intent data, while the invoice-currency paths call fiatCurrency.GetFiatCode() immediately. Either reject a nil fiat currency here in one place or make the non-nil invariant explicit at construction/persistence and guarded at this accessor.

🤖 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/models/costbasis/intent.go` around lines 133 - 160,
Update GetFiatCurrency to validate the FiatCurrency pointer returned by the
ModeDynamic, ModePinned, and ModeManual intent variants before returning
success; return an appropriate validation error when it is nil, while preserving
existing conversion errors and valid non-nil results.

Comment on lines +143 to +144
s.Require().NotNil(result.GatheringLineToCreate, "charge index %d", idx)
s.Require().Equal(currencyx.FiatCode("USD"), result.GatheringLineToCreate.Currency, "charge index %d", idx)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Also assert the converted usage amount.

These checks prove that the line is denominated in USD, but they would still pass if custom-currency usage were copied into the fiat line without applying the resolved manual or pinned cost-basis rate. Assert the generated amounts for those cases (and define the dynamic case) so this billing regression cannot slip through.

🤖 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/service/usagebased_costbasis_test.go` around lines
143 - 144, Extend the assertions in the usage-based cost-basis tests around
GatheringLineToCreate to verify the converted usage amounts, not only the USD
currency. Cover custom-currency cases using the resolved manual or pinned
cost-basis rate, and define the expected amount for the dynamic-rate case so the
generated fiat lines validate the conversion.

Source: Path instructions

Comment on lines +134 to 136
cur, err := stdLine.Currency.AsFiatCurrency()
if err != nil {
return fmt.Errorf("creating currency calculator: %w", err)

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Apply the cost-basis conversion before building the fiat invoice line.

AsFiatCurrency() only resolves the destination currency definition. The later mapping copies detailed-line amounts and Run.Totals without applying a custom-currency conversion rate, so custom-currency usage can be labeled in the resolved fiat currency with incorrect amounts. Convert detailed amounts, credits, and expected totals before WithCreditsApplied and RoundToPrecision, or reject this path until conversion is available.

🤖 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/usagebased/service/linemapper.go` around lines 134
- 136, Update the line-mapping flow around AsFiatCurrency to apply the
custom-currency conversion rate to detailed amounts, credits, and expected
totals before WithCreditsApplied and RoundToPrecision; otherwise reject
custom-currency input instead of emitting incorrectly labeled fiat amounts.

@turip
turip merged commit b3835db into main Jul 23, 2026
29 of 32 checks passed
@turip
turip deleted the feat/billing-fiat-currency-fields branch July 23, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/billing 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