@stacksjs/commerce 0.75.89 → 0.75.91

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.
@@ -0,0 +1,63 @@
1
+ /** Units still on the shelf once the basket is taken off it; null when stock is not counted. */
2
+ export declare function shelfLeft(product: Product, basket: BasketLine[], fields?: RegisterFields): number | null;
3
+ /** Whether one more can be rung up. */
4
+ export declare function canAdd(product: Product, basket: BasketLine[], fields?: RegisterFields): boolean;
5
+ /** One more of a product, when there is one to sell; the basket unchanged otherwise. */
6
+ export declare function addToBasket(basket: BasketLine[], product: Product, fields?: RegisterFields): BasketLine[];
7
+ /** A line's quantity, held to the stock on hand; zero (or less) takes the line off. */
8
+ export declare function setQuantity(basket: BasketLine[], product: Product, quantity: number, fields?: RegisterFields): BasketLine[];
9
+ /** The basket's lines with their products, leaving out any no longer in the catalog. */
10
+ export declare function basketLines<T extends Product>(basket: BasketLine[], products: T[], fields?: RegisterFields): Array<BasketLine & { product: T }>;
11
+ /** The basket's total, in minor units. */
12
+ export declare function basketTotal(basket: BasketLine[], products: Product[], fields?: RegisterFields): number;
13
+ /**
14
+ * The part of the basket's total that sales tax applies to: every line whose
15
+ * product is not marked `taxable: false` (a food product, an admission).
16
+ * Feed it to `breakdownFor` from `@stacksjs/commerce/sales-tax`.
17
+ */
18
+ export declare function basketTaxable(basket: BasketLine[], products: Product[], fields?: RegisterFields): number;
19
+ /** How many units are in the basket. */
20
+ export declare function basketCount(basket: BasketLine[]): number;
21
+ /**
22
+ * How a product's stock reads on the register: sold out, running low (at or
23
+ * under its own reorder level, `defaultLow` when it has none), or how many
24
+ * are left. Null when stock is not counted.
25
+ */
26
+ export declare function stockLabel(product: Product, basket?: BasketLine[], fields?: RegisterFields, defaultLow?: number): StockLabel | null;
27
+ /** The distinct values of a grouping (a shelf, a category), in the order the products come. */
28
+ export declare function groupsOf<T>(products: T[], groupOf: (product: T) => string): string[];
29
+ export declare const DEFAULT_FIELDS: RegisterFields;
30
+ /** Field names for a catalog stored with `price_cents` and `inventory_quantity`. */
31
+ export declare const CENTS_FIELDS: RegisterFields;
32
+ /**
33
+ * A point-of-sale register's basket: what is being rung up, held against what
34
+ * is on the shelf, before anything is written. Pure and dependency-free, so
35
+ * the browser bundles it (`@stacksjs/commerce/register`) and a server can
36
+ * check the same rules.
37
+ *
38
+ * A product is anything with an `id`, a `price` in minor units and, when its
39
+ * stock is counted, an `inventory` (null or absent: not counted, never runs
40
+ * out). Field names follow the caller: pass `fields` when a catalog stores
41
+ * them as, say, `price_cents` and `inventory_quantity`.
42
+ *
43
+ * ```ts
44
+ * import { addToBasket, basketTotal } from '@stacksjs/commerce/register'
45
+ *
46
+ * let basket = addToBasket([], gatorade)
47
+ * basket = addToBasket(basket, gatorade)
48
+ * basketTotal(basket, catalog) // 700
49
+ * ```
50
+ */
51
+ export declare interface BasketLine {
52
+ productId: number
53
+ quantity: number
54
+ }
55
+ export declare interface RegisterFields {
56
+ id: string
57
+ price: string
58
+ inventory: string
59
+ lowStock: string
60
+ taxable: string
61
+ }
62
+ export declare interface StockLabel { text: string, tone: 'out' | 'low' | 'ok' }
63
+ declare type Product = Record<string, any>;
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ var k=Object.defineProperty;var x=(t)=>t;function m(t,n){this[t]=x.bind(null,n)}var b=(t,n)=>{for(var e in n)k(t,e,{get:n[e],enumerable:!0,configurable:!0,set:m.bind(n,e)})};var h=(t,n,e)=>()=>{if(t)try{n=t(t=0)}catch(r){e=[r]}if(e)throw e[0];return n};var S={};b(S,{CENTS_FIELDS:()=>g,DEFAULT_FIELDS:()=>u,addToBasket:()=>L,basketCount:()=>B,basketLines:()=>l,basketTaxable:()=>I,basketTotal:()=>F,canAdd:()=>f,groupsOf:()=>T,setQuantity:()=>y,shelfLeft:()=>a,stockLabel:()=>R});var u={id:"id",price:"price",inventory:"inventory",lowStock:"low_stock_threshold",taxable:"taxable"},g={id:"id",price:"price_cents",inventory:"inventory_quantity",lowStock:"low_stock_threshold",taxable:"taxable"};function s(t,n){return Number(t[n.id])}function p(t,n){let e=t[n.inventory];return e===null||e===void 0||e===""?null:Number(e)}function a(t,n,e=u){let r=p(t,e);if(r===null)return null;let o=n.find((c)=>c.productId===s(t,e))?.quantity||0;return Math.max(0,r-o)}function f(t,n,e=u){let r=a(t,n,e);return r===null||r>0}function L(t,n,e=u){if(!f(n,t,e))return t;let r=s(n,e);return t.some((o)=>o.productId===r)?t.map((o)=>o.productId===r?{...o,quantity:o.quantity+1}:o):[...t,{productId:r,quantity:1}]}function y(t,n,e,r=u){let o=s(n,r),c=p(n,r),d=Math.min(Math.max(0,Math.floor(Number(e)||0)),c===null?Number.POSITIVE_INFINITY:c);if(d===0)return t.filter((i)=>i.productId!==o);return t.some((i)=>i.productId===o)?t.map((i)=>i.productId===o?{...i,quantity:d}:i):[...t,{productId:o,quantity:d}]}function l(t,n,e=u){return t.map((r)=>({...r,product:n.find((o)=>s(o,e)===r.productId)})).filter((r)=>!!r.product)}function F(t,n,e=u){return l(t,n,e).reduce((r,o)=>r+Number(o.product[e.price]||0)*o.quantity,0)}function I(t,n,e=u){return l(t,n,e).filter((r)=>r.product[e.taxable]!==!1).reduce((r,o)=>r+Number(o.product[e.price]||0)*o.quantity,0)}function B(t){return t.reduce((n,e)=>n+e.quantity,0)}function R(t,n=[],e=u,r=5){let o=a(t,n,e);if(o===null)return null;if(o===0)return{text:"Sold out",tone:"out"};let c=t[e.lowStock]===null||t[e.lowStock]===void 0?r:Number(t[e.lowStock]);return{text:`${o} left`,tone:o<=c?"low":"ok"}}function T(t,n){let e=[];for(let r of t){let o=n(r);if(!e.includes(o))e.push(o)}return e}export{g as CENTS_FIELDS,u as DEFAULT_FIELDS,L as addToBasket,B as basketCount,l as basketLines,I as basketTaxable,F as basketTotal,f as canAdd,T as groupsOf,y as setQuantity,a as shelfLeft,R as stockLabel};
@@ -0,0 +1,48 @@
1
+ export declare function isReleaseMode(value: unknown): value is ReleaseMode;
2
+ /** The product row's columns as a config, with the defaults filled in. */
3
+ export declare function rolloutFromProduct(product: { release_mode?: unknown, release_interval_days?: unknown, release_initial_count?: unknown, release_starts_at?: unknown }): RolloutConfig;
4
+ /** When unit `index` unlocks for a buyer whose clock starts at `purchasedAt`. */
5
+ export declare function unlockAt(config: RolloutConfig, index: number, purchasedAt: Date, override?: UnitOverride): Date;
6
+ /** The release of every unit, in order, and whether it is open at `now`. */
7
+ export declare function releaseSchedule(config: RolloutConfig, count: number, purchasedAt: Date, now: Date, overrides?: UnitOverride[]): UnitRelease[];
8
+ /**
9
+ * How many leading units are open. A plan's weeks are written to the calendar
10
+ * in order, so an early week never waits behind a later one that opened first
11
+ * (possible with per-video overrides, which plans do not have).
12
+ */
13
+ export declare function unlockedPrefix(schedule: UnitRelease[]): number;
14
+ /** A sentence for the product page: what a buyer gets, and when. */
15
+ export declare function describeRollout(config: RolloutConfig, count: number, unit: 'video' | 'week', now?: Date): string;
16
+ /**
17
+ * When each part of a digital product opens for a buyer: the videos of a
18
+ * series, the weeks of a training plan or a course. Pure and dependency-free,
19
+ * so the browser bundles it (`@stacksjs/commerce/releases`) and a server
20
+ * decides the same way.
21
+ *
22
+ * - `all_at_once`: every unit at the anchor.
23
+ * - `drip`: the first `initialCount` units at the anchor, then one more every
24
+ * `intervalDays`. A video's own `offsetDays` overrides its slot.
25
+ * - `scheduled`: the same dates for every buyer, from `startsAt`, stepping the
26
+ * same way. A video's own `releaseAt` overrides its slot. Someone buying
27
+ * late gets everything already released.
28
+ *
29
+ * The anchor is the purchase, but never before launch: a product whose
30
+ * `startsAt` is still ahead is a pre-sale, and nothing opens until then.
31
+ */
32
+ export declare const RELEASE_MODES: readonly ['all_at_once', 'drip', 'scheduled'];
33
+ export declare interface RolloutConfig {
34
+ mode: ReleaseMode
35
+ intervalDays: number
36
+ initialCount: number
37
+ startsAt: string | null
38
+ }
39
+ export declare interface UnitOverride {
40
+ offsetDays?: number | null
41
+ releaseAt?: string | null
42
+ }
43
+ export declare interface UnitRelease {
44
+ index: number
45
+ unlock_at: string
46
+ unlocked: boolean
47
+ }
48
+ export type ReleaseMode = typeof RELEASE_MODES[number];
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ var _=Object.defineProperty;var b=(e)=>e;function h(e,t){this[e]=b.bind(null,t)}var v=(e,t)=>{for(var n in t)_(e,n,{get:t[n],enumerable:!0,configurable:!0,set:h.bind(t,n)})};var A=(e,t,n)=>()=>{if(e)try{t=e(e=0)}catch(o){n=[o]}if(n)throw n[0];return t};var p={};v(p,{RELEASE_MODES:()=>D,describeRollout:()=>S,isReleaseMode:()=>f,releaseSchedule:()=>k,rolloutFromProduct:()=>w,unlockAt:()=>y,unlockedPrefix:()=>R});var D=["all_at_once","drip","scheduled"];function f(e){return typeof e==="string"&&D.includes(e)}function w(e){let t=Number(e.release_interval_days),n=Number(e.release_initial_count),o=s(e.release_starts_at);return{mode:f(e.release_mode)?e.release_mode:"all_at_once",intervalDays:Number.isInteger(t)&&t>0?t:7,initialCount:Number.isInteger(n)&&n>0?n:1,startsAt:o===null?null:new Date(o).toISOString()}}function s(e){if(e==null||e==="")return null;let t=new Date(String(e)).getTime();return Number.isFinite(t)?t:null}function d(e,t){return Math.max(0,t-(e.initialCount-1))}function y(e,t,n,o={}){let i=s(e.startsAt),a=Math.max(n.getTime(),i??0);if(e.mode==="scheduled"){let r=s(o.releaseAt);if(r!==null)return new Date(r);let l=i??n.getTime();return new Date(l+d(e,t)*e.intervalDays*86400000)}if(e.mode==="drip"){let r=o.offsetDays;if(r!=null&&Number.isFinite(Number(r))&&Number(r)>=0)return new Date(a+Number(r)*86400000);return new Date(a+d(e,t)*e.intervalDays*86400000)}return new Date(a)}function k(e,t,n,o,i=[]){let a=[];for(let r=0;r<t;r++){let l=y(e,r,n,i[r]||{});a.push({index:r,unlock_at:l.toISOString(),unlocked:l.getTime()<=o.getTime()})}return a}function R(e){let t=0;for(let n of e){if(!n.unlocked)break;t++}return t}function S(e,t,n,o=new Date){let i=(c)=>`${c} ${n}${c===1?"":"s"}`,a=s(e.startsAt),r=a!==null&&a>o.getTime()?` from ${new Date(a).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"})}`:"";if(t<=0)return"";if(t===1)return`Available${r||" immediately"}.`;if(e.mode==="all_at_once"||t<=e.initialCount)return`All ${i(t)} available${r||" immediately"}.`;let l=e.intervalDays===1?"every day":e.intervalDays===7?"every week":`every ${e.intervalDays} days`,u=e.initialCount===1,m=u?`The first ${n}`:`The first ${i(e.initialCount)}`;if(e.mode==="drip")return`${m} ${u?"unlocks":"unlock"}${r||" on purchase"}, then one more ${l}.`;if(r)return`${m} ${u?"releases":"release"} on ${r.slice(6)}, then one more ${l}, on the same dates for everyone.`;return`Releasing on a shared schedule${a===null?"":` since ${new Date(a).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric",timeZone:"UTC"})}`}, one more ${n} ${l}. Join now and everything already out is yours.`}export{D as RELEASE_MODES,S as describeRollout,f as isReleaseMode,k as releaseSchedule,w as rolloutFromProduct,y as unlockAt,R as unlockedPrefix};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A stored rate as a decimal multiplier.
3
+ *
4
+ * Rates are entered as percentages, because that is how tax is written down
5
+ * and how the dashboard asks for it. Multiplying by a percentage would charge
6
+ * a hundred times too much, which is the kind of error that reaches a
7
+ * customer's card before anyone notices.
8
+ */
9
+ export declare function multiplierOf(rate: unknown): number;
10
+ export declare function breakdownFor(taxable: number, rates: readonly TaxRateRow[], options?: BreakdownOptions): TaxBreakdown;
11
+ /**
12
+ * What tax is owed on an amount, component by component.
13
+ *
14
+ * The tax rates a dashboard manages did not price anything. `recomputeOrderTotals`
15
+ * takes a single `taxRateId`, multiplies once, and returns one number — so a
16
+ * business whose tax is a sum of parts had to blend them into one rate before
17
+ * storing it, and a blended rate cannot answer the question that matters when
18
+ * a customer is exempt: exempt from *which part*?
19
+ *
20
+ * Most jurisdictions tax in parts and lift some of them for some buyers.
21
+ * Groceries escape VAT but not a deposit levy. A Californian medical cannabis
22
+ * patient is exempt from sales tax and still pays excise and the city's
23
+ * business tax. Blending those into one number leaves an application choosing
24
+ * between over-charging the exempt customer and under-collecting tax it owes.
25
+ *
26
+ * So: rates are rows, an exemption lifts the ones marked `exemptible`, and the
27
+ * result itemises what was charged and what was not.
28
+ *
29
+ * The calculation is pure and imports nothing, so a register in the browser
30
+ * shows the tax before it charges (`@stacksjs/commerce/sales-tax`); the rates
31
+ * a dashboard manages are read by `taxFor` in the tax module.
32
+ */
33
+ /** One component, as it applied to this sale. */
34
+ export declare interface TaxComponent {
35
+ id: number
36
+ code: string
37
+ name: string
38
+ rate: number
39
+ amount: number
40
+ exempted: boolean
41
+ }
42
+ export declare interface TaxBreakdown {
43
+ taxable: number
44
+ components: TaxComponent[]
45
+ tax: number
46
+ exempted: number
47
+ }
48
+ export declare interface BreakdownOptions {
49
+ exempt?: boolean
50
+ codes?: string[]
51
+ country?: string
52
+ }
53
+ /**
54
+ * Tax on `taxable` cents, itemised.
55
+ *
56
+ * Each component is rounded on its own rather than the total being rounded
57
+ * once. That is what a receipt has to show — the parts have to add up to the
58
+ * figure charged, and a single rounding at the end leaves a line that is a
59
+ * cent out from the sum above it.
60
+ *
61
+ * Every component is applied to `taxable` directly. Jurisdictions that compound
62
+ * — where one tax forms part of the base for another — need an explicit order,
63
+ * and that is a bigger claim than this should make quietly; an app that needs
64
+ * it should compose two calls.
65
+ */
66
+ /** One tax rate row, in the terms the breakdown reads it. */
67
+ export declare interface TaxRateRow {
68
+ id?: number | string
69
+ name?: string
70
+ code?: string
71
+ rate?: number | string
72
+ exemptible?: boolean
73
+ }
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ function s(t){let n=Number(t);return Number.isFinite(n)?n/100:0}function b(t,n,p={}){let a=Math.max(0,Math.round(t)),m=[],i=0,u=0;for(let e of n){let x=s(e.rate),o=Math.round(a*x),r=Boolean(p.exempt&&e.exemptible);if(r)u+=o;else i+=o;m.push({id:Number(e.id),code:String(e.code??""),name:String(e.name??""),rate:x,amount:r?0:o,exempted:r})}return{taxable:a,components:m,tax:i,exempted:u}}export{b as breakdownFor,s as multiplierOf};
@@ -1,6 +1,8 @@
1
+ import { breakdownFor } from '../sales-tax';
2
+ import type { BreakdownOptions, TaxBreakdown } from '../sales-tax';
3
+ export type { BreakdownOptions, TaxBreakdown, TaxComponent, TaxRateRow } from '../sales-tax';
1
4
  /** Every active rate, in the order they should be listed. */
2
5
  export declare function activeTaxRates(options?: BreakdownOptions): Promise<any[]>;
3
- export declare function breakdownFor(taxable: number, rates: readonly TaxRateRow[], options?: BreakdownOptions): TaxBreakdown;
4
6
  /**
5
7
  * Read the active rates and apply them.
6
8
  *
@@ -9,44 +11,4 @@ export declare function breakdownFor(taxable: number, rates: readonly TaxRateRow
9
11
  * per bag rather than querying each time.
10
12
  */
11
13
  export declare function taxFor(taxable: number, options?: BreakdownOptions): Promise<TaxBreakdown>;
12
- /** One component, as it applied to this sale. */
13
- export declare interface TaxComponent {
14
- id: number
15
- code: string
16
- name: string
17
- rate: number
18
- amount: number
19
- exempted: boolean
20
- }
21
- export declare interface TaxBreakdown {
22
- taxable: number
23
- components: TaxComponent[]
24
- tax: number
25
- exempted: number
26
- }
27
- export declare interface BreakdownOptions {
28
- exempt?: boolean
29
- codes?: string[]
30
- country?: string
31
- }
32
- /**
33
- * Tax on `taxable` cents, itemised.
34
- *
35
- * Each component is rounded on its own rather than the total being rounded
36
- * once. That is what a receipt has to show — the parts have to add up to the
37
- * figure charged, and a single rounding at the end leaves a line that is a
38
- * cent out from the sum above it.
39
- *
40
- * Every component is applied to `taxable` directly. Jurisdictions that compound
41
- * — where one tax forms part of the base for another — need an explicit order,
42
- * and that is a bigger claim than this should make quietly; an app that needs
43
- * it should compose two calls.
44
- */
45
- /** One tax rate row, in the terms the breakdown reads it. */
46
- export declare interface TaxRateRow {
47
- id?: number | string
48
- name?: string
49
- code?: string
50
- rate?: number | string
51
- exemptible?: boolean
52
- }
14
+ export { breakdownFor } from '../sales-tax';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/commerce",
3
3
  "type": "module",
4
- "version": "0.75.89",
4
+ "version": "0.75.91",
5
5
  "description": "Stacks commerce utilities.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -57,17 +57,17 @@
57
57
  "prepublishOnly": "bun run build"
58
58
  },
59
59
  "dependencies": {
60
- "@stacksjs/cache": "0.75.89",
61
- "@stacksjs/config": "0.75.89",
62
- "@stacksjs/database": "0.75.89",
63
- "@stacksjs/env": "0.75.89",
64
- "@stacksjs/error-handling": "0.75.89",
65
- "@stacksjs/events": "0.75.89",
66
- "@stacksjs/logging": "0.75.89",
67
- "@stacksjs/orm": "0.75.89",
68
- "@stacksjs/payments": "0.75.89",
69
- "@stacksjs/realtime": "0.75.89",
70
- "@stacksjs/strings": "0.75.89",
60
+ "@stacksjs/cache": "0.75.91",
61
+ "@stacksjs/config": "0.75.91",
62
+ "@stacksjs/database": "0.75.91",
63
+ "@stacksjs/env": "0.75.91",
64
+ "@stacksjs/error-handling": "0.75.91",
65
+ "@stacksjs/events": "0.75.91",
66
+ "@stacksjs/logging": "0.75.91",
67
+ "@stacksjs/orm": "0.75.91",
68
+ "@stacksjs/payments": "0.75.91",
69
+ "@stacksjs/realtime": "0.75.91",
70
+ "@stacksjs/strings": "0.75.91",
71
71
  "ts-spreadsheets": "^0.2.0"
72
72
  },
73
73
  "devDependencies": {