impl/typescript.ts
2,741 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { compareMoney } from "./money_compare.ts"; ← from money.compare ^1.0.0 · built alongside by fune
import { chargeableWeight } from "./retail_volumetric_weight.ts"; ← from retail.volumetric-weight ^1.0.0 · built alongside by fune
import { type ShippingBand, type Parcel, type ShippingQuote } from "./retail_shipping_rate_types.ts";
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
function chargeableIn(band: ShippingBand, parcel: Parcel): number {
if (band.volumetricDivisor === null) {
return roundDiv(parcel.actualGrams, band.roundUpToGrams, "up") * band.roundUpToGrams;
}
return chargeableWeight(
parcel.lengthMm,
parcel.widthMm,
parcel.heightMm,
parcel.actualGrams,
band.volumetricDivisor,
band.roundUpToGrams,
).chargeableGrams;
}
/**
* The delivery charge for a parcel: the smallest band on the rate card in
* force on the order date that takes it, free when the basket reaches the
* band's threshold.
*/
export function shippingRate(
bands: readonly ShippingBand[],
service: string,
zone: string,
parcel: Parcel,
basketTotal: Money,
onDate: string,
): ShippingQuote {
if (!ISO_DATE.test(onDate)) throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
for (const d of [parcel.lengthMm, parcel.widthMm, parcel.heightMm]) {
if (!Number.isInteger(d) || d < 1) throw new RangeError(`dimensions must be 1 mm or more, received ${d}`);
}
if (!Number.isInteger(parcel.actualGrams) || parcel.actualGrams < 0) {
throw new RangeError(`actualGrams must not be negative, received ${parcel.actualGrams}`);
}
const longest = Math.max(parcel.lengthMm, parcel.widthMm, parcel.heightMm);
let best: ShippingBand | null = null;
let bestGrams = 0;
for (const band of bands) {
if (band.service !== service || band.zone !== zone) continue;
if (onDate < band.validFrom || (band.validTo !== null && onDate > band.validTo)) continue;
if (band.maxLengthMm !== null && longest > band.maxLengthMm) continue;
const grams = chargeableIn(band, parcel);
if (grams > band.maxGrams) continue;
if (
best === null ||
band.maxGrams < best.maxGrams ||
(band.maxGrams === best.maxGrams && compareMoney(band.price, best.price) < 0)
) {
best = band;
bestGrams = grams;
}
}
if (best === null) {
throw new RangeError(`no shipping band for service "${service}" to zone "${zone}" on ${onDate} fits this parcel`);
}
const free = best.freeOver !== null && compareMoney(basketTotal, best.freeOver) >= 0;
return {
service,
zone,
chargeableGrams: bestGrams,
bandMaxGrams: best.maxGrams,
standardPrice: best.price,
price: free ? money(0, best.price.currency) : best.price,
free,
};
}