@cellarnode/finance
Canonical price-range Zod schemas, BASIS enum, currency helpers, format helpers, and inheritance helpers for CellarNode. Headless TypeScript — no React, no UI deps.
Install
pnpm add @cellarnode/finance zod
# or
npm install @cellarnode/finance zod
# or
bun add @cellarnode/finance zod
zod is a peer dependency.
Exports
import {
// BASIS
BASIS, type Basis,
// Price ranges
priceRangeSchema, type PriceRange,
fingerprintRange,
// Chosen price ranges (with rangeRef + label)
chosenPriceRangeSchema, type ChosenPriceRange,
// Inheritance
resolveEffective,
type DefaultPriceProfile,
type EffectiveRange,
// Currency
isValidCurrencyCode,
getCurrencyName,
getMinorUnits,
// Formatting
formatMoney,
formatBasis,
formatPriceRange,
} from "@cellarnode/finance";
Quick examples
Validate a price range
import { priceRangeSchema } from "@cellarnode/finance";
const range = priceRangeSchema.parse({
min: 12.0,
max: 18.0,
currency: "EUR",
basis: "per_unit",
priceTypeId: "fob",
priceTypeOther: null,
basisNote: null,
});
Validation rules (superRefine):
max >= minpriceTypeIdandpriceTypeOtherare mutually exclusivebasisNoteis required iffbasis === "other"
Inherit defaults from a portfolio profile
import { resolveEffective } from "@cellarnode/finance";
const range = { min: 1, max: 2, currency: null, basis: null, priceTypeId: null, priceTypeOther: null, basisNote: null };
const profile = { currency: "EUR", basis: "per_unit", priceTypeId: "fob" };
const effective = resolveEffective(range, profile);
// → { min:1, max:2, currency:"EUR", basis:"per_unit", priceTypeId:"fob", ... }
Format a price range for display
import { formatPriceRange } from "@cellarnode/finance";
formatPriceRange({ min: 12, max: 18, currency: "EUR", basis: "per_unit", ... });
// → "€12.00 - €18.00 per unit"
Compare a normalized price amount
import { compareNormalizedPriceAmount } from "@cellarnode/finance";
const result = compareNormalizedPriceAmount({
amount: 7.5,
min: null,
max: 10,
fromBasis: "per_unit",
toBasis: "per_litre",
packageVolumeMl: 750,
fxRate: 1,
});
// { ok: true, relation: "within", comparisonAmount: 10 }
Bounds are inclusive, finite, nonnegative and ordered; null opens one side.
Both bounds cannot be null. Amount must be finite and nonnegative, and FX rate
must be positive and finite (pass 1 for the same currency). A supplied package
volume must be positive and finite, and is required when the bases differ.
Only canonical per_unit and per_litre bases are accepted by the input type.
Each operand means its String(number) decimal spelling, including exponents.
The relation uses exact rational arithmetic without currency rounding or tolerance.
The informational comparisonAmount is rounded to a JavaScript number only after
comparison; it may equal a bound even when the exact relation is outside it.
Overflow or a positive result rounding to zero returns
{ ok: false, reason: "unrepresentable_amount" }. Other failure reasons are
invalid_amount, invalid_bounds, invalid_fx_rate, and invalid_package_volume.
No BigInt values leave the helper.
The caller owns currency identity, FX selection/freshness, trusted package volume, qualified case identity, and unconstrained-range handling. This helper performs no I/O, mutation, scoring or policy evaluation.
Why this package?
Before @cellarnode/finance, the canonical price-range Zod schema lived in cellarnode-backend-v2/src/features/opportunities/price-range-schema.ts. Three dashboards (admin, producer, importer) duplicated the validation rules — one drift was already observed during Spec 1 implementation. This package owns the wire format end-to-end so all consumers stay in sync.
Development
make build # clean + typecheck + compile
make test # vitest run
make test-watch # vitest watch
make typecheck # tsc --noEmit
make check-exports # publint + arethetypeswrong
Publishing
Published to npm (public access — publishConfig.access: "public", see CEL-1983) via npm Trusted Publishing (OIDC) —
.github/workflows/publish.yml publishes automatically on a merge to main that changes
package.json's version. There is no long-lived npm token in the publish job, and nobody runs
npm publish by hand. This is a private repo, so unlike @cellarnode/beverage-utils, npm does
not generate provenance attestations for this package.
Cut a release locally, then land it through a normal PR:
make release-patch # bump patch, commit, tag
make release-minor # bump minor, commit, tag
make release-major # bump major, commit, tag
These targets bump package.json, build, commit, and tag — they do not publish. Push the
branch, open a PR, and merging to main triggers CI's publish job.
License
UNLICENSED — Copyright (c) CellarNode. All rights reserved. This package is published for CellarNode's own applications; no license is granted for other use.