|
| 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, and future persisted rounding configuration. |
| 38 | +- **Finance boundary**: snapshots fiat basis and applies fiat rounding when custom units become fiat amounts. |
| 39 | +- **Ledger boundary**: records durable currency codes and balanced single-currency legs. Round before posting only when the upstream domain owns normalization. |
| 40 | +- **Invoice boundary**: invoice currency stays fiat. Custom units must be materialized to fiat before invoice artifacts. |
| 41 | + |
| 42 | +## Rounding Rules |
| 43 | + |
| 44 | +- Preserve fiat rounding unless the task explicitly changes fiat money behavior. |
| 45 | +- Custom currency default rounding is `RoundingModeBankers` (`RoundBank`, half-even). |
| 46 | +- Custom currencies can opt into `RoundingModeHalfAwayFromZero` through `NewCustomCurrencyWithRounding`. |
| 47 | +- `Calculator.RoundToPrecision` is the single place that applies the effective rounding mode. |
| 48 | +- `Calculator.RoundDown` and `Calculator.Unit` are precision helpers; they should not apply banker/half-away rounding. |
| 49 | +- `Calculator.IsRoundedToPrecision` must use `RoundToPrecision`, so it follows the configured rounding mode. |
| 50 | + |
| 51 | +## Process |
| 52 | + |
| 53 | +1. Name the surface before editing: code validation, type/interface, rounding, calculator, allocation, registry, ledger fact, fiat materialization, or invoice boundary. |
| 54 | +2. Keep `pkg/currencyx` free of imports from `openmeter/...`; callers can implement `currencyx.Currency` to supply registry-backed custom settings. |
| 55 | +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. |
| 56 | +4. Preserve `currencyx.Code(...).Calculator()` for existing fiat callers. |
| 57 | +5. For custom currencies, validate structural code, route delimiter exclusion, fiat-code collisions, precision, and rounding mode. |
| 58 | +6. Keep allocation deterministic: precision defines units, largest remainder distributes residual units, and tie-breakers remain stable. |
| 59 | +7. After editing, run focused `pkg/currencyx` tests, `go vet`, and caller tests or compile checks for every touched boundary. |
| 60 | + |
| 61 | +## Test Checklist |
| 62 | + |
| 63 | +Cover the named risk introduced by the change: |
| 64 | + |
| 65 | +- Fiat regression behavior: code validation, precision from ISO definition, and existing rounding. |
| 66 | +- Validation boundaries: `Code.Validate()` stays fiat-only while `ValidateFormat()` accepts structurally valid custom codes. |
| 67 | +- Custom interface behavior: code, type, precision, and rounding mode all flow through `currencyx.Currency`. |
| 68 | +- Banker ties: positive, negative, and zero-precision custom rounding tie to even. |
| 69 | +- Alternate custom rounding: half-away-from-zero remains selectable and tested. |
| 70 | +- Invalid config: bad precision or rounding mode fails validation. |
| 71 | +- Allocation precision: custom precision affects units and largest-remainder allocation. |
| 72 | +- Boundary tests: billing/invoice rejects custom invoice currency explicitly; ledger accepts structurally valid custom codes only when that domain supports them. |
| 73 | + |
| 74 | +Focused commands: |
| 75 | + |
| 76 | +```bash |
| 77 | +env GOCACHE=/private/tmp/openmeter-go-build go test ./pkg/currencyx |
| 78 | +env GOCACHE=/private/tmp/openmeter-go-build go vet ./pkg/currencyx |
| 79 | +``` |
| 80 | + |
| 81 | +For caller compile checks, keep the package list scoped to touched boundaries and include `-tags=dynamic` when billing/ledger paths require it. |
| 82 | + |
| 83 | +## Review Checks |
| 84 | + |
| 85 | +- Fiat and custom currencies share the `currencyx.Currency` interface. |
| 86 | +- `Calculator.RoundToPrecision` applies the effective rounding rule. |
| 87 | +- `Calculator` does not require fiat definitions for custom currencies. |
| 88 | +- Allocation code uses calculator methods, not fiat-only definition fields. |
| 89 | +- Invalid rounding precision or mode fails validation. |
| 90 | +- Tests cover banker ties, configured custom precision, fiat regression behavior, invalid rounding config, and allocation precision. |
0 commit comments