Skip to content

Commit 7d26845

Browse files
feat(currencies): refactor currencies package
1 parent 83fe614 commit 7d26845

50 files changed

Lines changed: 2361 additions & 1051 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/currencyx/SKILL.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
name: currencyx
3+
description: Work on OpenMeter currency primitives in pkg/currencyx for fiat and custom currency codes, shared currency interfaces, rounding modes, calculators, allocation precision, fiat/custom boundaries, and callers in billing, charges, ledger, product catalog, subscriptions, API, or currency registry code.
4+
---
5+
6+
# Currencyx
7+
8+
Use this skill when changes touch `pkg/currencyx` or any caller that depends on currency code shape, fiat/custom classification, calculator behavior, rounding, allocation, or invoice/ledger currency boundaries.
9+
10+
Also load the domain skill for each touched caller area: `billing`, `charges`, `ledger`, `subscription`, `api`, `ent`, `db-migration`, and `test`.
11+
12+
## Source Of Truth
13+
14+
- Source code: `pkg/currencyx/*.go`.
15+
- Primary tests: `pkg/currencyx/*_test.go`.
16+
- Package usage examples: `pkg/currencyx/README.md`.
17+
- This skill is a how-to/reference for agents. Update it whenever `Currency`, `Code`, `CustomCurrency`, `Calculator`, rounding, allocation, or validation behavior changes.
18+
- One canonical repo skill lives at `.agents/skills/currencyx`; do not create duplicate currencyx guidance elsewhere.
19+
20+
## Package Layout
21+
22+
- `currency.go`: currency type constants, the shared `Currency` interface, `Code`, `CustomCurrency`, and fiat/custom constructors.
23+
- `validation.go`: code format validation, fiat collision checks, precision validation, `PostgresCodeSchemaType`, and `CustomCurrency.Validate`.
24+
- `fiat.go`: rounding modes, `Calculator`, and precision helpers.
25+
- `allocation.go`: deterministic largest-remainder allocation using calculator precision.
26+
- `README.md`: short examples for fiat, custom, and allocation usage.
27+
28+
## Boundary Model
29+
30+
- **Currency code**: durable identifier. Fiat and custom codes use `currencyx.Code`, but validation differs by boundary.
31+
- **Currency interface**: shared behavior contract. Callers that know a configured currency should expose `CurrencyCode()`, `CurrencyType()`, `CurrencyPrecision()`, and `CurrencyRoundingMode()`.
32+
- **Fiat currency**: `currencyx.Code` implements `currencyx.Currency` as fiat. `Code.Calculator()` preserves existing fiat behavior and derives precision from GOBL/ISO definitions.
33+
- **Custom currency**: `currencyx.CustomCurrency` implements `currencyx.Currency`. Custom currencies carry configured precision and rounding mode; missing rounding mode defaults to bankers rounding.
34+
- **Calculator**: construct with `currencyx.NewCalculator(currencyx.Currency)`. The calculator must branch from `CurrencyType()` and use `CurrencyPrecision()` / `CurrencyRoundingMode()` from the interface for custom currencies.
35+
- **Allocation**: use calculator precision for units and largest-remainder distribution. Do not reach into fiat-only `Def.Subunits` for allocation logic.
36+
- **Validation**: `Code.Validate()` remains fiat semantic for existing callers. Use `ValidateFormat()` for structural code checks and `ValidateCustom()` for custom currency codes. Custom codes must not contain the `|` route delimiter.
37+
- **Registry boundary**: owns custom currency definition, fiat-code collision checks, archive/activation rules, cost-basis history, and future persisted rounding configuration.
38+
- **Cost basis history**: entries are effective-dated with `effective_from` and optional `effective_to`; API responses expose the cost-basis `id`. Use `currency_id` terminology in domain/schema models, even when the current route is under custom currencies. If renaming old `custom_currency_id` storage, use data-preserving migrations.
39+
- **Finance boundary**: snapshots fiat basis and applies fiat rounding when custom units become fiat amounts.
40+
- **Ledger boundary**: records durable currency codes and balanced single-currency legs. Round before posting only when the upstream domain owns normalization.
41+
- **Invoice boundary**: invoice currency stays fiat. Custom units must be materialized to fiat before invoice artifacts.
42+
43+
## Rounding Rules
44+
45+
- Preserve fiat rounding unless the task explicitly changes fiat money behavior.
46+
- Custom currency default rounding is `RoundingModeBankers` (`RoundBank`, half-even).
47+
- Custom currencies can opt into `RoundingModeHalfAwayFromZero` through `NewCustomCurrencyWithRounding`.
48+
- `Calculator.RoundToPrecision` is the single place that applies the effective rounding mode.
49+
- `Calculator.RoundDown` and `Calculator.Unit` are precision helpers; they should not apply banker/half-away rounding.
50+
- `Calculator.IsRoundedToPrecision` must use `RoundToPrecision`, so it follows the configured rounding mode.
51+
52+
## Process
53+
54+
1. Name the surface before editing: code validation, type/interface, rounding, calculator, allocation, registry, ledger fact, fiat materialization, or invoice boundary.
55+
2. Keep `pkg/currencyx` free of imports from `openmeter/...`; callers can implement `currencyx.Currency` to supply registry-backed custom settings.
56+
3. Prefer `CurrencyType()` at the boundary that truly requires fiat or custom. Do not add broad split helpers unless the caller boundary needs a named domain rule.
57+
4. Preserve `currencyx.Code(...).Calculator()` for existing fiat callers.
58+
5. For custom currencies, validate structural code, route delimiter exclusion, fiat-code collisions, precision, and rounding mode.
59+
6. Keep allocation deterministic: precision defines units, largest remainder distributes residual units, and tie-breakers remain stable.
60+
7. After editing, run focused `pkg/currencyx` tests, `go vet`, and caller tests or compile checks for every touched boundary.
61+
62+
## Test Checklist
63+
64+
Cover the named risk introduced by the change:
65+
66+
- Fiat regression behavior: code validation, precision from ISO definition, and existing rounding.
67+
- Validation boundaries: `Code.Validate()` stays fiat-only while `ValidateFormat()` accepts structurally valid custom codes.
68+
- Custom interface behavior: code, type, precision, and rounding mode all flow through `currencyx.Currency`.
69+
- Banker ties: positive, negative, and zero-precision custom rounding tie to even.
70+
- Alternate custom rounding: half-away-from-zero remains selectable and tested.
71+
- Invalid config: bad precision or rounding mode fails validation.
72+
- Allocation precision: custom precision affects units and largest-remainder allocation.
73+
- Boundary tests: billing/invoice rejects custom invoice currency explicitly; ledger accepts structurally valid custom codes only when that domain supports them.
74+
75+
Focused commands:
76+
77+
```bash
78+
env GOCACHE=/private/tmp/openmeter-go-build go test ./pkg/currencyx
79+
env GOCACHE=/private/tmp/openmeter-go-build go vet ./pkg/currencyx
80+
```
81+
82+
For caller compile checks, keep the package list scoped to touched boundaries and include `-tags=dynamic` when billing/ledger paths require it.
83+
84+
## Review Checks
85+
86+
- Fiat and custom currencies share the `currencyx.Currency` interface.
87+
- `Calculator.RoundToPrecision` applies the effective rounding rule.
88+
- `Calculator` does not require fiat definitions for custom currencies.
89+
- Allocation code uses calculator methods, not fiat-only definition fields.
90+
- Invalid rounding precision or mode fails validation.
91+
- Tests cover banker ties, configured custom precision, fiat regression behavior, invalid rounding config, and allocation precision.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface:
2+
display_name: "Currencyx"
3+
short_description: "Work on fiat and custom currency primitives"
4+
default_prompt: "Use $currencyx to update OpenMeter currency code, rounding, calculator, allocation, or fiat/custom boundary behavior."

api/spec/packages/aip-client-javascript/src/models/schemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1782,6 +1782,7 @@ export const costBasis = z
17821782
fiat_code: currencyCode,
17831783
rate: numeric,
17841784
effective_from: dateTime.optional(),
1785+
effective_to: dateTime.optional(),
17851786
created_at: dateTime,
17861787
})
17871788
.describe('Describes currency basis supported by billing system.')
@@ -1791,6 +1792,7 @@ export const createCostBasisRequest = z
17911792
fiat_code: currencyCode,
17921793
rate: numeric,
17931794
effective_from: dateTime.optional(),
1795+
effective_to: dateTime.optional(),
17941796
})
17951797
.describe('CostBasis create request.')
17961798

api/spec/packages/aip-client-javascript/src/models/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,12 @@ export interface CostBasis {
968968
* `now` by the system.
969969
*/
970970
effective_from?: string
971+
/**
972+
* An ISO-8601 timestamp representation of the date until which the cost basis is
973+
* effective. If provided, it must be later than `effective_from`. If not provided,
974+
* it remains effective until superseded.
975+
*/
976+
effective_to?: string
971977
/** An ISO-8601 timestamp representation of entity creation date. */
972978
created_at: string
973979
}
@@ -984,6 +990,12 @@ export interface CreateCostBasisRequest {
984990
* `now` by the system.
985991
*/
986992
effective_from?: string
993+
/**
994+
* An ISO-8601 timestamp representation of the date until which the cost basis is
995+
* effective. If provided, it must be later than `effective_from`. If not provided,
996+
* it remains effective until superseded.
997+
*/
998+
effective_to?: string
987999
}
9881000

9891001
/** A row in the result of a feature cost query. */

api/spec/packages/aip/src/currencies/cost-bases/cost-basis.tsp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ model CostBasis {
3131
@visibility(Lifecycle.Create, Lifecycle.Read)
3232
effective_from?: Shared.DateTime;
3333

34+
/**
35+
* An ISO-8601 timestamp representation of the date until which the cost basis is
36+
* effective. If provided, it must be later than `effective_from`. If not provided,
37+
* it remains effective until superseded.
38+
*/
39+
@visibility(Lifecycle.Create, Lifecycle.Read)
40+
effective_to?: Shared.DateTime;
41+
3442
/**
3543
* An ISO-8601 timestamp representation of entity creation date.
3644
*/

0 commit comments

Comments
 (0)