web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit 7fb108082fa6a85910ff3a2542eec03f3c56f576
parent a28190f04989014d85795b2ac751fb3289bfa248
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Sun, 27 Apr 2025 03:38:39 +0000

utils: add listing order utils, add/edit utils

Diffstat:
Mutils/package.json | 1+
Mutils/src/currency.ts | 3+++
Mutils/src/index.ts | 1+
Mutils/src/lib.ts | 17+++++++++++++++--
Autils/src/listings/order.ts | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mutils/src/unit.ts | 15+++++++++++++++
6 files changed, 180 insertions(+), 2 deletions(-)

diff --git a/utils/package.json b/utils/package.json @@ -34,6 +34,7 @@ "@nostr-dev-kit/ndk": "^2.11.0", "@sveltekit-i18n/base": "^1.3.7", "@sveltekit-i18n/parser-icu": "^1.0.8", + "@radroots/radroots-common-bindings": "workspace:*", "convert": "^5.5.1", "geohashing": "^2.0.1", "nostr-geotags": "workspace:*", diff --git a/utils/src/currency.ts b/utils/src/currency.ts @@ -3,6 +3,9 @@ import { util_rxp } from "$root"; export type FiatCurrency = `usd` | `eur`; export const fiat_currencies: FiatCurrency[] = [`usd`, `eur`] as const; +// @todo +export const price_to_formatted = (n: number, _currency: string) => Math.round(n * 100) / 100; + export const parse_currency = (val?: string): FiatCurrency => { const cur = val?.trim().toLowerCase() switch (cur) { diff --git a/utils/src/index.ts b/utils/src/index.ts @@ -19,6 +19,7 @@ export * from "./i18n" export * from "./image" export * from "./lib" export * from "./list" +export * from "./listings/order" export * from "./model" export * from "./number" export * from "./object" diff --git a/utils/src/lib.ts b/utils/src/lib.ts @@ -1,10 +1,11 @@ import { type CallbackPromise } from "./types"; -export const ascii = { +export const symbols = { bullet: '•', dash: `—`, up: `↑`, - down: `↓` + down: `↓`, + percent: `%` } export const root_symbol = "»--`--,---"; @@ -51,4 +52,16 @@ export const exe_iter = async (callback: CallbackPromise, num: number = 1, delay } catch (e) { console.log(`(error) exe_iter `, e); } +}; + + +export const fmt_tags_key = (...args: (string | number | undefined)[]) => + args.filter(Boolean).join("-").toLowerCase(); + +export const compare_str_eq = (a: string, b: string): boolean => { + return a.toLowerCase() === b.toLowerCase(); +}; + +export const compare_str_ne = (a: string, b: string): boolean => { + return a.toLowerCase() !== b.toLowerCase(); }; \ No newline at end of file diff --git a/utils/src/listings/order.ts b/utils/src/listings/order.ts @@ -0,0 +1,145 @@ +import { compare_str_ne, fmt_tags_key, mass_to_g, parse_mass_unit, price_to_formatted, symbols } from "$root"; +import type { ListingOrder, ListingOrderDiscount } from "@radroots/radroots-common-bindings"; + +export type IListingOrderCreate = { + tags_prices: string[][]; + tags_quantities: string[][]; + tags_discounts: string[][]; + quantity: { + amount: number; + unit: string; + count: number; + }; +}; + +export const listing_order_create = (opts: IListingOrderCreate): ListingOrder => { + const { + tags_prices, tags_quantities, tags_discounts, + quantity: { amount: qty_amt, unit: qty_unit, count: qty_count } + } = opts; + + const qty_key = fmt_tags_key(qty_amt, qty_unit); + const qty_tag = tags_quantities.find(t => fmt_tags_key(t[1], t[2]) === qty_key); + if (!qty_tag) throw new Error(`invalid quantity tag`); + + const [, qty_tag_amt, qty_tag_unit, qty_tag_label] = qty_tag; + const qty_unit_parsed = parse_mass_unit(qty_tag_unit); + if (!qty_unit_parsed) throw new Error(`invalid quantity unit`); + + const price_qty_key = fmt_tags_key(qty_tag_amt, qty_tag_unit, qty_tag_label); + const price_tag = tags_prices.find(t => t[5]?.toLowerCase() === price_qty_key); + if (!price_tag) throw new Error(`invalid price tag`); + + const [, price_tag_amt, price_tag_currency, price_tag_qty_amt, price_tag_qty_unit] = price_tag; + const price_unit_parsed = parse_mass_unit(price_tag_qty_unit); + if (!price_unit_parsed) throw new Error(`invalid price unit`); + + const qty_amt_num = parseFloat(qty_tag_amt); + const price_amt = parseFloat(price_tag_amt); + const price_qty_amt = parseFloat(price_tag_qty_amt); + + const mass_g = mass_to_g(qty_amt_num * qty_count, qty_tag_unit); + const price_group_g = mass_to_g(price_qty_amt, price_tag_qty_unit); + const group_count = mass_g / price_group_g; + const subtotal = price_to_formatted(group_count * price_amt, price_tag_currency); + + const discounts: ListingOrderDiscount[] = []; + + for (const discount of tags_discounts) { + const discount_type = discount[0]; + + if (discount_type === `price-discount-subtotal`) { + const [, threshold_str, currency, value_str, mode] = discount; + if (compare_str_ne(currency, price_tag_currency)) continue; + const threshold = parseFloat(threshold_str); + if (subtotal < threshold) continue; + + if (mode === symbols.percent) { + const percent = parseFloat(value_str); + discounts.push({ + discount_type: `subtotal`, + threshold, + discount_percent: percent, + discount_amount: price_to_formatted(subtotal * percent / 100, currency), + currency + }); + } else { + const amount = parseFloat(value_str); + discounts.push({ + discount_type: `subtotal`, + threshold, + discount_amount: price_to_formatted(amount, currency), + currency + }); + } + } + + if (discount_type === `price-discount-mass`) { + const [, discount_unit, threshold_str, threshold_unit, per_unit_str, currency] = discount; + if (compare_str_ne(currency, price_tag_currency)) continue; + const threshold = parseFloat(threshold_str); + const mass_in_threshold = mass_g / mass_to_g(1, threshold_unit); + if (mass_in_threshold < threshold) continue; + + const per_unit = parseFloat(per_unit_str); + const unit_count = mass_g / mass_to_g(1, discount_unit); + discounts.push({ + discount_type: `mass`, + threshold, + threshold_unit, + discount_per_unit: per_unit, + discount_unit, + discount_amount: price_to_formatted(unit_count * per_unit, currency), + currency + }); + } + + if (discount_type === `price-discount-quantity`) { + const [, key, min_str, per_unit_str, currency] = discount; + if (compare_str_ne(currency, price_tag_currency)) continue; + if (key !== price_qty_key) continue; + const min = parseInt(min_str); + if (qty_count < min) continue; + const per_unit = parseFloat(per_unit_str); + discounts.push({ + discount_type: `quantity`, + threshold: min, + discount_per_unit: per_unit, + discount_amount: price_to_formatted(per_unit * qty_count, currency), + currency + }); + } + } + + const total_discount = price_to_formatted(discounts.reduce((acc, d) => acc + d.discount_amount, 0), price_tag_currency); + const subtotal_amt = price_to_formatted(subtotal, price_tag_currency); + const total_amt = price_to_formatted(subtotal - total_discount, price_tag_currency); + + return { + quantity: { + amount: qty_amt_num, + unit: qty_unit_parsed, + label: qty_tag_label, + count: qty_count, + }, + price: { + amount: price_amt, + currency: price_tag_currency, + quantity_amount: price_qty_amt, + quantity_unit: price_unit_parsed + }, + discounts, + subtotal: { + price_amount: subtotal_amt, + price_currency: price_tag_currency, + quantity_amount: qty_amt_num * qty_count, + quantity_unit: qty_tag_unit + }, + total: { + price_amount: total_amt, + price_currency: price_tag_currency, + quantity_amount: qty_amt_num * qty_count, + quantity_unit: qty_tag_unit + } + }; +}; diff --git a/utils/src/unit.ts b/utils/src/unit.ts @@ -23,6 +23,21 @@ export function parse_mass_unit(val?: string): MassUnit | undefined { }; }; +export function mass_to_g(val: number, unit: string): number { + const mass_unit = parse_mass_unit(unit); + switch (mass_unit) { + case `kg`: + return val * 1000; + case `lb`: + return val * 453.592; + case `g`: + return val; + default: + throw new Error(`unsupported unit ${unit}`); + } +} + + export function parse_area_unit_default(val?: string): AreaUnit { const unit = parse_area_unit(val); return unit ?? `ac`