@classytic/primitives 0.26.2 → 0.26.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/address.d.mts +2 -2
- package/dist/address.mjs +2 -1
- package/dist/payment-gateway.d.mts +19 -0
- package/dist/price-basis.d.mts +31 -0
- package/dist/price-basis.mjs +51 -0
- package/dist/rounded-div-BWJb98Ws.mjs +13 -0
- package/dist/unit-cost-rate.d.mts +9 -3
- package/dist/unit-cost-rate.mjs +1 -14
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
4
|
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## 0.26.4
|
|
7
|
+
|
|
8
|
+
### Added — `./price-basis` subpath: measured pricing across physical dimensions
|
|
9
|
+
|
|
10
|
+
New `PriceBasis` type (`FixedPriceBasis | MeasuredPriceBasis`) models how a quoted price spans a sellable quantity — flat fixed or per-unit-of-measure (mass, duration, volume, length, area). Factory functions `fixedPriceBasis()` and `measuredPriceBasis(dimension, qtyUnit, priceUnit, qtyPerPriceUnit)` construct the two shapes; `isPriceBasis()` is a structural guard for DB/wire boundaries. `resolvePriceBasisTotal(unitPriceMinor, quantity, basis?)` converts a minor-unit price × integer quantity to a final minor-unit total using half-even rounding (`roundedDiv` internally) and throws `PriceBasisError` on unsafe inputs or overflow.
|
|
11
|
+
|
|
12
|
+
### Added — `./unit-cost-rate` re-exports `RoundingMode`
|
|
13
|
+
|
|
14
|
+
`RoundingMode` (`'half-even' | 'half-up'`) is now defined in the new internal `rounded-div.ts` and re-exported from `unit-cost-rate`. No breaking change — existing consumers who import `RoundingMode` from `@classytic/primitives/unit-cost-rate` continue to work.
|
|
15
|
+
|
|
16
|
+
### Changed — `validateAddress` / `isAddressValid` accept `Address` directly
|
|
17
|
+
|
|
18
|
+
Both functions now accept `Address | PartialAddress | null | undefined`, removing the need for a cast when validating a fully-typed value object.
|
|
19
|
+
|
|
20
|
+
## 0.26.3
|
|
21
|
+
|
|
22
|
+
### Added — `PaymentProviderPort.wantsRawBody`
|
|
23
|
+
|
|
24
|
+
Optional `readonly wantsRawBody?: boolean` on the payment gateway port.
|
|
25
|
+
Providers that verify an HMAC signature (Stripe signs the bytes it transmitted)
|
|
26
|
+
must declare `true` — the transport then hands the raw body rather than the
|
|
27
|
+
parsed object. Absent / `false` preserves the historical behaviour (parsed
|
|
28
|
+
body), which is correct for manual, cash and terminal providers that sign
|
|
29
|
+
nothing.
|
|
30
|
+
|
|
6
31
|
## 0.26.2
|
|
7
32
|
|
|
8
33
|
### Added — `stockKeyFor` (`/item-identity`): one stock-key precedence for every package
|
package/dist/address.d.mts
CHANGED
|
@@ -298,9 +298,9 @@ interface AddressFieldError {
|
|
|
298
298
|
* `' '` satisfies a naive truthiness check and then prints as a blank line on
|
|
299
299
|
* a courier label. Required means a value, not a character.
|
|
300
300
|
*/
|
|
301
|
-
declare function validateAddress(address: PartialAddress | null | undefined, format: AddressFormat): AddressFieldError[];
|
|
301
|
+
declare function validateAddress(address: Address | PartialAddress | null | undefined, format: AddressFormat): AddressFieldError[];
|
|
302
302
|
/** Convenience for a submit gate. */
|
|
303
|
-
declare function isAddressValid(address: PartialAddress | null | undefined, format: AddressFormat): boolean;
|
|
303
|
+
declare function isAddressValid(address: Address | PartialAddress | null | undefined, format: AddressFormat): boolean;
|
|
304
304
|
/** A readable default message, in the country's own vocabulary. */
|
|
305
305
|
declare function describeAddressError(error: AddressFieldError): string;
|
|
306
306
|
//#endregion
|
package/dist/address.mjs
CHANGED
|
@@ -162,7 +162,8 @@ function supportedCountries(registry) {
|
|
|
162
162
|
*/
|
|
163
163
|
function validateAddress(address, format) {
|
|
164
164
|
const errors = [];
|
|
165
|
-
const
|
|
165
|
+
const values = address;
|
|
166
|
+
const value = (k) => String(values?.[k] ?? "").trim();
|
|
166
167
|
/**
|
|
167
168
|
* Canonical and extension fields are checked IDENTICALLY, keyed by
|
|
168
169
|
* `fieldName`. A required Delivery Area is enforced exactly as a required
|
|
@@ -277,6 +277,25 @@ interface PaymentProviderPort {
|
|
|
277
277
|
* without this cannot be auto-reconciled — the engine then RETAINS the reservation.
|
|
278
278
|
*/
|
|
279
279
|
getRefundStatus?(query: RefundStatusQuery, command?: PaymentCommandContext): Promise<PaymentResult>;
|
|
280
|
+
/**
|
|
281
|
+
* Does this provider need the EXACT transmitted bytes, rather than the
|
|
282
|
+
* parsed body?
|
|
283
|
+
*
|
|
284
|
+
* `true` for anything that verifies an HMAC signature — Stripe signs the
|
|
285
|
+
* bytes it sent, and a re-serialized object differs in key order, spacing and
|
|
286
|
+
* unicode escaping, so a check against the parsed form can never pass.
|
|
287
|
+
*
|
|
288
|
+
* Declared by the provider rather than decided by the transport, because the
|
|
289
|
+
* transport cannot know. Handing raw bytes to every provider breaks the ones
|
|
290
|
+
* that read `payload.sessionId` — they get a string and silently see
|
|
291
|
+
* `undefined`, so the webhook resolves to no transaction and the payment
|
|
292
|
+
* never applies. Handing the parsed body to every provider breaks signature
|
|
293
|
+
* verification. Only the provider knows which it is.
|
|
294
|
+
*
|
|
295
|
+
* Absent means `false`: the historical behaviour, and correct for manual,
|
|
296
|
+
* cash and terminal providers that sign nothing.
|
|
297
|
+
*/
|
|
298
|
+
readonly wantsRawBody?: boolean;
|
|
280
299
|
handleWebhook(payload: unknown, headers?: Record<string, string>): Promise<WebhookEvent>;
|
|
281
300
|
/**
|
|
282
301
|
* Reject a forged webhook. Returning `true` unconditionally is only acceptable for a
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/money/price-basis.d.ts
|
|
2
|
+
/** How a quoted price extends across a sellable quantity. */
|
|
3
|
+
type PriceBasis = FixedPriceBasis | MeasuredPriceBasis;
|
|
4
|
+
interface FixedPriceBasis {
|
|
5
|
+
readonly kind: 'fixed';
|
|
6
|
+
}
|
|
7
|
+
type MeasureDimension = 'mass' | 'duration' | 'volume' | 'length' | 'area';
|
|
8
|
+
interface MeasuredPriceBasis {
|
|
9
|
+
readonly kind: 'measured';
|
|
10
|
+
readonly dimension: MeasureDimension;
|
|
11
|
+
/** Integer unit carried by the line quantity, such as `gram` or `minute`. */
|
|
12
|
+
readonly quantityUnit: string;
|
|
13
|
+
/** Unit named by the quoted price, such as `kilogram` or `hour`. */
|
|
14
|
+
readonly priceUnit: string;
|
|
15
|
+
/** Number of quantity units represented by one quoted price unit. */
|
|
16
|
+
readonly quantityPerPriceUnit: number;
|
|
17
|
+
}
|
|
18
|
+
type PriceBasisErrorCode = 'INVALID_PRICE' | 'INVALID_QUANTITY' | 'INVALID_DENOMINATOR' | 'INVALID_UNIT' | 'UNSAFE_TOTAL';
|
|
19
|
+
declare class PriceBasisError extends Error {
|
|
20
|
+
readonly name = "PriceBasisError";
|
|
21
|
+
readonly code: PriceBasisErrorCode;
|
|
22
|
+
constructor(code: PriceBasisErrorCode, message: string);
|
|
23
|
+
}
|
|
24
|
+
declare function fixedPriceBasis(): FixedPriceBasis;
|
|
25
|
+
declare function measuredPriceBasis(dimension: MeasureDimension, quantityUnit: string, priceUnit: string, quantityPerPriceUnit: number): MeasuredPriceBasis;
|
|
26
|
+
/** Structural guard for database and wire boundaries. */
|
|
27
|
+
declare function isPriceBasis(value: unknown): value is PriceBasis;
|
|
28
|
+
/** Extend a minor-unit price using integer quantity and exact half-even rounding. */
|
|
29
|
+
declare function resolvePriceBasisTotal(unitPriceMinor: number, quantity: number, basis?: PriceBasis): number;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { FixedPriceBasis, MeasureDimension, MeasuredPriceBasis, PriceBasis, PriceBasisError, PriceBasisErrorCode, fixedPriceBasis, isPriceBasis, measuredPriceBasis, resolvePriceBasisTotal };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { t as roundedDiv } from "./rounded-div-BWJb98Ws.mjs";
|
|
2
|
+
//#region src/money/price-basis.ts
|
|
3
|
+
var PriceBasisError = class extends Error {
|
|
4
|
+
name = "PriceBasisError";
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function fixedPriceBasis() {
|
|
12
|
+
return { kind: "fixed" };
|
|
13
|
+
}
|
|
14
|
+
function measuredPriceBasis(dimension, quantityUnit, priceUnit, quantityPerPriceUnit) {
|
|
15
|
+
if (quantityUnit.trim() === "" || priceUnit.trim() === "") throw new PriceBasisError("INVALID_UNIT", "quantityUnit and priceUnit must not be empty");
|
|
16
|
+
if (!Number.isSafeInteger(quantityPerPriceUnit) || quantityPerPriceUnit <= 0) throw new PriceBasisError("INVALID_DENOMINATOR", "quantityPerPriceUnit must be a positive safe integer");
|
|
17
|
+
return {
|
|
18
|
+
kind: "measured",
|
|
19
|
+
dimension,
|
|
20
|
+
quantityUnit,
|
|
21
|
+
priceUnit,
|
|
22
|
+
quantityPerPriceUnit
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const MEASURE_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
26
|
+
"mass",
|
|
27
|
+
"duration",
|
|
28
|
+
"volume",
|
|
29
|
+
"length",
|
|
30
|
+
"area"
|
|
31
|
+
]);
|
|
32
|
+
/** Structural guard for database and wire boundaries. */
|
|
33
|
+
function isPriceBasis(value) {
|
|
34
|
+
if (typeof value !== "object" || value === null) return false;
|
|
35
|
+
const candidate = value;
|
|
36
|
+
if (candidate.kind === "fixed") return true;
|
|
37
|
+
return candidate.kind === "measured" && MEASURE_DIMENSIONS.has(candidate.dimension) && typeof candidate.quantityUnit === "string" && candidate.quantityUnit.trim() !== "" && typeof candidate.priceUnit === "string" && candidate.priceUnit.trim() !== "" && typeof candidate.quantityPerPriceUnit === "number" && Number.isSafeInteger(candidate.quantityPerPriceUnit) && candidate.quantityPerPriceUnit > 0;
|
|
38
|
+
}
|
|
39
|
+
/** Extend a minor-unit price using integer quantity and exact half-even rounding. */
|
|
40
|
+
function resolvePriceBasisTotal(unitPriceMinor, quantity, basis = fixedPriceBasis()) {
|
|
41
|
+
if (!Number.isSafeInteger(unitPriceMinor) || unitPriceMinor < 0) throw new PriceBasisError("INVALID_PRICE", "unitPriceMinor must be a non-negative safe integer");
|
|
42
|
+
if (!Number.isSafeInteger(quantity) || quantity < 0) throw new PriceBasisError("INVALID_QUANTITY", "quantity must be a non-negative safe integer");
|
|
43
|
+
const numerator = BigInt(unitPriceMinor) * BigInt(quantity);
|
|
44
|
+
const denominator = basis.kind === "measured" ? BigInt(basis.quantityPerPriceUnit) : 1n;
|
|
45
|
+
const rounded = roundedDiv(numerator, denominator, "half-even");
|
|
46
|
+
const total = Number(rounded);
|
|
47
|
+
if (!Number.isSafeInteger(total)) throw new PriceBasisError("UNSAFE_TOTAL", "resolved total exceeds Number.MAX_SAFE_INTEGER");
|
|
48
|
+
return total;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { PriceBasisError, fixedPriceBasis, isPriceBasis, measuredPriceBasis, resolvePriceBasisTotal };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/money/rounded-div.ts
|
|
2
|
+
/** Exact signed integer division rounded at the rational boundary. */
|
|
3
|
+
function roundedDiv(numerator, denominator, mode) {
|
|
4
|
+
if (denominator <= 0n) throw new RangeError("denominator must be positive");
|
|
5
|
+
const negative = numerator < 0n;
|
|
6
|
+
const magnitude = negative ? -numerator : numerator;
|
|
7
|
+
const quotient = magnitude / denominator;
|
|
8
|
+
const twiceRemainder = magnitude % denominator * 2n;
|
|
9
|
+
const rounded = twiceRemainder > denominator || twiceRemainder === denominator && (mode === "half-up" || quotient % 2n !== 0n) ? quotient + 1n : quotient;
|
|
10
|
+
return negative ? -rounded : rounded;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { roundedDiv as t };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { t as MinorUnits, w as CurrencyCode } from "./money-CfAvfNiM.mjs";
|
|
2
|
+
//#region src/money/rounded-div.d.ts
|
|
3
|
+
type RoundingMode = 'half-even' | 'half-up';
|
|
4
|
+
//#endregion
|
|
2
5
|
//#region src/money/unit-cost-rate.d.ts
|
|
3
6
|
/** Fixed, ecosystem-wide rate scale: 6 decimal places below the minor unit. */
|
|
4
7
|
declare const RATE_SCALE = 1000000;
|
|
5
|
-
/** Half-to-even (banker's) is the default; half-up available for legacy parity. */
|
|
6
|
-
type RoundingMode = 'half-even' | 'half-up';
|
|
7
8
|
/**
|
|
8
9
|
* Minor units per unit, stored as an integer scaled by {@link RATE_SCALE}.
|
|
9
10
|
* `scale` is carried so a value is self-describing on the wire even if
|
|
@@ -67,5 +68,10 @@ declare function weightedAverageScaledRate(onHandQuantity: number, onHandScaledR
|
|
|
67
68
|
declare function rateMinorPerUnit(rate: UnitCostRate): number;
|
|
68
69
|
/** Structural guard. */
|
|
69
70
|
declare function isUnitCostRate(value: unknown): value is UnitCostRate;
|
|
71
|
+
/**
|
|
72
|
+
* Rounded integer division of two bigints, `numerator / denominator`
|
|
73
|
+
* (`denominator > 0`), honouring the rounding mode at the exact half.
|
|
74
|
+
* Sign-safe for negative numerators.
|
|
75
|
+
*/
|
|
70
76
|
//#endregion
|
|
71
|
-
export { RATE_SCALE, RoundingMode, UnitCostRate, UnitCostRateError, UnitCostRateErrorCode, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal, weightedAverageScaledRate };
|
|
77
|
+
export { RATE_SCALE, type RoundingMode, UnitCostRate, UnitCostRateError, UnitCostRateErrorCode, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal, weightedAverageScaledRate };
|
package/dist/unit-cost-rate.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MinorUnits } from "./money.mjs";
|
|
2
|
+
import { t as roundedDiv } from "./rounded-div-BWJb98Ws.mjs";
|
|
2
3
|
//#region src/money/unit-cost-rate.ts
|
|
3
4
|
/** Fixed, ecosystem-wide rate scale: 6 decimal places below the minor unit. */
|
|
4
5
|
const RATE_SCALE = 1e6;
|
|
@@ -127,19 +128,5 @@ function isUnitCostRate(value) {
|
|
|
127
128
|
* (`denominator > 0`), honouring the rounding mode at the exact half.
|
|
128
129
|
* Sign-safe for negative numerators.
|
|
129
130
|
*/
|
|
130
|
-
function roundedDiv(numerator, denominator, mode) {
|
|
131
|
-
const neg = numerator < 0n;
|
|
132
|
-
const n = neg ? -numerator : numerator;
|
|
133
|
-
const q = n / denominator;
|
|
134
|
-
const remainder = n - q * denominator;
|
|
135
|
-
if (remainder === 0n) return neg ? -q : q;
|
|
136
|
-
const twiceRem = remainder * 2n;
|
|
137
|
-
let up;
|
|
138
|
-
if (twiceRem > denominator) up = true;
|
|
139
|
-
else if (twiceRem < denominator) up = false;
|
|
140
|
-
else up = mode === "half-up" ? true : q % 2n !== 0n;
|
|
141
|
-
const result = up ? q + 1n : q;
|
|
142
|
-
return neg ? -result : result;
|
|
143
|
-
}
|
|
144
131
|
//#endregion
|
|
145
132
|
export { RATE_SCALE, UnitCostRateError, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal, weightedAverageScaledRate };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.4",
|
|
4
4
|
"description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
|
|
5
5
|
"author": "Classytic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"types": "./dist/unit-cost-rate.d.mts",
|
|
34
34
|
"default": "./dist/unit-cost-rate.mjs"
|
|
35
35
|
},
|
|
36
|
+
"./price-basis": {
|
|
37
|
+
"types": "./dist/price-basis.d.mts",
|
|
38
|
+
"default": "./dist/price-basis.mjs"
|
|
39
|
+
},
|
|
36
40
|
"./address": {
|
|
37
41
|
"types": "./dist/address.d.mts",
|
|
38
42
|
"default": "./dist/address.mjs"
|