@lacspace/order 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence β€” a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ <div align="center">
2
+
3
+ # @lacspace/order
4
+
5
+ **A headless order-lifecycle engine β€” an immutable order model with a state machine, price snapshotting & timestamped history.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@lacspace/order?color=%2316a34a&label=npm)](https://www.npmjs.com/package/@lacspace/order)
8
+ [![minzipped](https://img.shields.io/bundlephobia/minzip/@lacspace/order?label=minzip)](https://bundlephobia.com/package/@lacspace/order)
9
+ [![types](https://img.shields.io/badge/types-included-blue)](https://www.npmjs.com/package/@lacspace/order)
10
+ [![license](https://img.shields.io/npm/l/@lacspace/order?color=green)](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
11
+
12
+ </div>
13
+
14
+ > The order spine between a cart and a courier. A tiny set of **pure functions** over a plain `Order` object β€” snapshot line prices, drive a status **state machine**, mint order numbers, and keep a **timestamped history**. No React, no store, no floats.
15
+
16
+ - 🧊 **Immutable** β€” every op returns a brand-new order; your input is never mutated
17
+ - πŸ” **State machine** β€” a validated status graph (`pending β†’ placed β†’ paid β†’ … β†’ completed`)
18
+ - πŸ“Έ **Snapshotted** β€” line prices are frozen at order time, so catalogue changes never rewrite history
19
+ - πŸͺ™ **Exact money** β€” integer **minor units** everywhere, so you never lose a penny
20
+ - πŸ’Ύ **Serializable** β€” `Order` is plain data, safe to `JSON.stringify` and persist
21
+ - ⚑ Isomorphic β€” Node, edge runtimes & browsers Β· πŸ“¦ ESM + CJS Β· fully typed Β· zero deps
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm i @lacspace/order # or pnpm add / yarn add / bun add
27
+ ```
28
+
29
+ ## Create an order
30
+
31
+ ```ts
32
+ import { createOrder } from "@lacspace/order";
33
+
34
+ const order = createOrder({
35
+ currency: "USD",
36
+ customer: { id: "u_1", name: "Ada", email: "ada@example.com" },
37
+ lines: [
38
+ { sku: "tee", name: "Tee", unitPrice: 1999, qty: 2, taxRate: 0.2 }, // $19.99
39
+ { sku: "cap", name: "Cap", unitPrice: 999, qty: 1, taxRate: 0.2 },
40
+ ],
41
+ discount: 500, // minor units
42
+ shipping: 999,
43
+ });
44
+
45
+ order.totals; // { subtotal, discount, tax, shipping, total } β€” all integer minor units
46
+ order.status; // "pending"
47
+ order.lines[0].total; // 3998 β€” snapshotted at order time
48
+ ```
49
+
50
+ `total = subtotal - discount + tax + shipping`, clamped to never go below `0`. Tax defaults to the sum of `round(line.total * line.taxRate)` per line, or you can pass an explicit `tax` in minor units.
51
+
52
+ ## Drive the lifecycle
53
+
54
+ ```ts
55
+ import { transition } from "@lacspace/order";
56
+
57
+ let o = createOrder({ currency: "USD", lines });
58
+ o = transition(o, "placed", { note: "checkout complete" });
59
+ o = transition(o, "paid", { at: Date.now() });
60
+ o = transition(o, "processing");
61
+ // illegal jumps throw:
62
+ transition(o, "delivered"); // OrderError { code: "invalid-transition" }
63
+
64
+ o.history; // [{ status, at, note? }, …] β€” one event per hop
65
+ ```
66
+
67
+ ### The status graph
68
+
69
+ `pending β†’ placed β†’ paid β†’ processing β†’ fulfilled β†’ shipped β†’ delivered β†’ completed`, with `on_hold`, `cancelled` and `refunded` branches. `completed`, `cancelled` and `refunded` are terminal.
70
+
71
+ ## Edit before payment, lock after
72
+
73
+ ```ts
74
+ import { addLine, updateQty, removeLine } from "@lacspace/order";
75
+
76
+ let o = transition(createOrder({ currency: "USD", lines }), "placed");
77
+ o = addLine(o, { sku: "sticker", unitPrice: 100, qty: 3 }); // totals recomputed
78
+ o = updateQty(o, "sticker", 1); // qty 0 removes the line
79
+
80
+ o = transition(o, "paid");
81
+ addLine(o, { sku: "x", unitPrice: 1, qty: 1 }); // OrderError { code: "locked" }
82
+ ```
83
+
84
+ ## Numbering
85
+
86
+ ```ts
87
+ import { orderNumber, randomOrderId } from "@lacspace/order";
88
+
89
+ orderNumber(1); // "ORD-20260905-0001" (deterministic given seq + date)
90
+ orderNumber(42, { prefix: "INV", pad: 6, separator: "/" }); // "INV/20260905/000042"
91
+ randomOrderId({ prefix: "ord" }); // "ord_k3f9x1a7q2mz" β€” crypto-random short id
92
+ ```
93
+
94
+ ## API
95
+
96
+ | Export | Description |
97
+ | --- | --- |
98
+ | `createOrder(input)` | build an immutable order; snapshots line totals, computes totals, seeds history |
99
+ | `transition(order, to, opts?)` | validated status change; appends a history event; throws on illegal moves |
100
+ | `addLine` / `removeLine` / `updateQty` | edit lines & recompute totals β€” allowed only while `pending`/`placed` |
101
+ | `canTransition(from, to)` / `isTerminal(s)` | raw state-machine queries |
102
+ | `canCancel` / `canRefund` / `canShip` / `canFulfill` | convenience predicates over an order |
103
+ | `orderNumber(seq, opts?)` | deterministic human order number, e.g. `"ORD-20260905-0001"` |
104
+ | `randomOrderId(opts?)` | crypto-random short id (falls back to `Math.random` with a warning) |
105
+ | `ORDER_TRANSITIONS` | the status β†’ allowed-next-statuses map |
106
+ | `OrderError` | thrown with codes `"invalid-transition"` / `"locked"` |
107
+
108
+ Types exported: `OrderStatus`, `OrderLine`, `OrderLineInput`, `StatusEvent`, `OrderTotals`, `Order`, `CreateOrderInput`.
109
+
110
+ ## Licensing
111
+
112
+ This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** β€” permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
113
+
114
+ Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
115
+
116
+ <!-- LACSPACE-DEV-PLATFORM -->
117
+
118
+ ---
119
+
120
+ ## The Lacspace Developer Platform
121
+
122
+ `@lacspace/order` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
123
+
124
+ - πŸ—‚οΈ **All packages** β€” https://developer.lacspace.com/packages
125
+ - 🧭 **Developer handbook** β€” https://developer.lacspace.com/handbook
126
+ - πŸ§ͺ **Live playground** β€” https://developer.lacspace.com/playground
127
+ - πŸ–₯️ **Finished app templates** β€” https://templates.lacspace.com
128
+ - πŸš€ **Scaffold a full app** β€” `npm create lacspace-app@latest`
129
+
130
+ Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** β€” a permissive, free-to-use licence.
package/dist/index.cjs ADDED
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var ORDER_TRANSITIONS = {
5
+ pending: ["placed", "cancelled"],
6
+ placed: ["paid", "cancelled", "on_hold"],
7
+ paid: ["processing", "refunded", "on_hold", "cancelled"],
8
+ processing: ["fulfilled", "on_hold", "cancelled", "refunded"],
9
+ fulfilled: ["shipped", "refunded"],
10
+ shipped: ["delivered", "refunded"],
11
+ delivered: ["completed", "refunded"],
12
+ on_hold: ["placed", "paid", "processing", "cancelled"],
13
+ completed: [],
14
+ cancelled: [],
15
+ refunded: []
16
+ };
17
+ function canTransition(from, to) {
18
+ return ORDER_TRANSITIONS[from].includes(to);
19
+ }
20
+ function isTerminal(status) {
21
+ return ORDER_TRANSITIONS[status].length === 0;
22
+ }
23
+ var OrderError = class extends Error {
24
+ constructor(message, code) {
25
+ super(message);
26
+ this.name = "OrderError";
27
+ this.code = code;
28
+ }
29
+ };
30
+ function canCancel(order) {
31
+ return canTransition(order.status, "cancelled");
32
+ }
33
+ function canRefund(order) {
34
+ return canTransition(order.status, "refunded");
35
+ }
36
+ function canShip(order) {
37
+ return canTransition(order.status, "shipped");
38
+ }
39
+ function canFulfill(order) {
40
+ return canTransition(order.status, "fulfilled");
41
+ }
42
+ function snapshotLine(input) {
43
+ const qty = Math.max(0, Math.trunc(input.qty));
44
+ const unitPrice = Math.trunc(input.unitPrice);
45
+ const line = {
46
+ id: input.id ?? input.sku,
47
+ sku: input.sku,
48
+ name: input.name ?? input.sku,
49
+ unitPrice,
50
+ qty,
51
+ total: unitPrice * qty
52
+ };
53
+ if (input.taxRate !== void 0) line.taxRate = input.taxRate;
54
+ if (input.meta !== void 0) line.meta = input.meta;
55
+ return line;
56
+ }
57
+ function computeTotals(lines, opts) {
58
+ const subtotal = lines.reduce((sum, l) => sum + l.total, 0);
59
+ const discount = Math.max(0, Math.trunc(opts.discount ?? 0));
60
+ const shipping = Math.max(0, Math.trunc(opts.shipping ?? 0));
61
+ const tax = opts.tax !== void 0 ? Math.max(0, Math.trunc(opts.tax)) : lines.reduce((sum, l) => sum + Math.round(l.total * (l.taxRate ?? 0)), 0);
62
+ const total = Math.max(0, subtotal - discount + tax + shipping);
63
+ return { subtotal, discount, tax, shipping, total };
64
+ }
65
+ function createOrder(input) {
66
+ const now = input.now ?? Date.now();
67
+ const lines = input.lines.map(snapshotLine);
68
+ const totals = computeTotals(lines, input);
69
+ const status = input.status ?? "pending";
70
+ const id = input.id ?? randomOrderId({ prefix: "ord" });
71
+ const order = {
72
+ id,
73
+ number: input.number ?? id,
74
+ status,
75
+ currency: input.currency,
76
+ lines,
77
+ totals,
78
+ history: [{ status, at: now }],
79
+ createdAt: now,
80
+ updatedAt: now
81
+ };
82
+ if (input.customer !== void 0) order.customer = input.customer;
83
+ if (input.meta !== void 0) order.meta = input.meta;
84
+ return order;
85
+ }
86
+ function transition(order, to, opts) {
87
+ if (!canTransition(order.status, to)) {
88
+ throw new OrderError(
89
+ `Cannot transition order from "${order.status}" to "${to}".`,
90
+ "invalid-transition"
91
+ );
92
+ }
93
+ const at = opts?.at ?? Date.now();
94
+ const event = { status: to, at };
95
+ if (opts?.note !== void 0) event.note = opts.note;
96
+ return {
97
+ ...order,
98
+ status: to,
99
+ history: [...order.history, event],
100
+ updatedAt: at
101
+ };
102
+ }
103
+ var EDITABLE_STATUSES = ["pending", "placed"];
104
+ function assertEditable(order) {
105
+ if (!EDITABLE_STATUSES.includes(order.status)) {
106
+ throw new OrderError(
107
+ `Order is locked for editing in status "${order.status}"; lines can only change while pending or placed.`,
108
+ "locked"
109
+ );
110
+ }
111
+ }
112
+ function reprice(order, lines, now) {
113
+ const at = now ?? Date.now();
114
+ return {
115
+ ...order,
116
+ lines,
117
+ totals: computeTotals(lines, {
118
+ discount: order.totals.discount,
119
+ shipping: order.totals.shipping
120
+ }),
121
+ updatedAt: at
122
+ };
123
+ }
124
+ function addLine(order, line, now) {
125
+ assertEditable(order);
126
+ return reprice(order, [...order.lines, snapshotLine(line)], now);
127
+ }
128
+ function removeLine(order, lineId, now) {
129
+ assertEditable(order);
130
+ return reprice(
131
+ order,
132
+ order.lines.filter((l) => l.id !== lineId),
133
+ now
134
+ );
135
+ }
136
+ function updateQty(order, lineId, qty, now) {
137
+ assertEditable(order);
138
+ const next = Math.max(0, Math.trunc(qty));
139
+ const lines = order.lines.map(
140
+ (l) => l.id === lineId ? { ...l, qty: next, total: l.unitPrice * next } : l
141
+ ).filter((l) => l.qty > 0);
142
+ return reprice(order, lines, now);
143
+ }
144
+ function pad(n, width) {
145
+ const s = String(Math.abs(Math.trunc(n)));
146
+ return s.length >= width ? s : "0".repeat(width - s.length) + s;
147
+ }
148
+ function orderNumber(seq, opts) {
149
+ const prefix = opts?.prefix ?? "ORD";
150
+ const date = opts?.date ?? /* @__PURE__ */ new Date();
151
+ const width = opts?.pad ?? 4;
152
+ const sep = opts?.separator ?? "-";
153
+ const y = date.getFullYear();
154
+ const m = pad(date.getMonth() + 1, 2);
155
+ const d = pad(date.getDate(), 2);
156
+ return `${prefix}${sep}${y}${m}${d}${sep}${pad(seq, width)}`;
157
+ }
158
+ var ID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
159
+ function randomBytes(out) {
160
+ const g = typeof globalThis !== "undefined" ? globalThis : {};
161
+ if (g.crypto && typeof g.crypto.getRandomValues === "function") {
162
+ g.crypto.getRandomValues(out);
163
+ return out;
164
+ }
165
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
166
+ console.warn(
167
+ "[@lacspace/order] globalThis.crypto is unavailable; falling back to Math.random for randomOrderId (not cryptographically secure)."
168
+ );
169
+ }
170
+ for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);
171
+ return out;
172
+ }
173
+ function randomOrderId(opts) {
174
+ const bytes = randomBytes(new Uint8Array(16));
175
+ let out = "";
176
+ for (let i = 0; i < bytes.length; i++) {
177
+ out += ID_ALPHABET[bytes[i] % ID_ALPHABET.length];
178
+ }
179
+ const prefix = opts?.prefix;
180
+ return prefix ? `${prefix}_${out}` : out;
181
+ }
182
+
183
+ exports.ORDER_TRANSITIONS = ORDER_TRANSITIONS;
184
+ exports.OrderError = OrderError;
185
+ exports.addLine = addLine;
186
+ exports.canCancel = canCancel;
187
+ exports.canFulfill = canFulfill;
188
+ exports.canRefund = canRefund;
189
+ exports.canShip = canShip;
190
+ exports.canTransition = canTransition;
191
+ exports.createOrder = createOrder;
192
+ exports.isTerminal = isTerminal;
193
+ exports.orderNumber = orderNumber;
194
+ exports.randomOrderId = randomOrderId;
195
+ exports.removeLine = removeLine;
196
+ exports.transition = transition;
197
+ exports.updateQty = updateQty;
198
+ //# sourceMappingURL=index.cjs.map
199
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/order β€” a headless order-lifecycle engine.\n *\n * The spine between a cart and a courier: a pure, immutable and serializable\n * `Order` model with a state machine, order-number generation, line-item price\n * snapshotting, and a timestamped status history. Every operation returns a\n * brand-new `Order`; the input is never mutated, so the state plays nicely with\n * React, Redux, Zustand, signals, or a plain JSON column in your database.\n *\n * All money is expressed in **integer minor units** (cents, paise, satoshi…).\n * There are no floats stored anywhere, so you never lose a penny to rounding.\n *\n * Isomorphic β€” no Node built-ins. The only platform API touched is\n * `globalThis.crypto` for random ids, and that access is guarded.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Status + state machine */\n/* -------------------------------------------------------------------------- */\n\n/** Every status an order can hold across its lifecycle. */\nexport type OrderStatus =\n | \"pending\"\n | \"placed\"\n | \"paid\"\n | \"processing\"\n | \"fulfilled\"\n | \"shipped\"\n | \"delivered\"\n | \"completed\"\n | \"on_hold\"\n | \"cancelled\"\n | \"refunded\";\n\n/**\n * The allowed forward transitions for each status. Terminal statuses\n * (`completed`, `cancelled`, `refunded`) map to an empty array.\n */\nexport const ORDER_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {\n pending: [\"placed\", \"cancelled\"],\n placed: [\"paid\", \"cancelled\", \"on_hold\"],\n paid: [\"processing\", \"refunded\", \"on_hold\", \"cancelled\"],\n processing: [\"fulfilled\", \"on_hold\", \"cancelled\", \"refunded\"],\n fulfilled: [\"shipped\", \"refunded\"],\n shipped: [\"delivered\", \"refunded\"],\n delivered: [\"completed\", \"refunded\"],\n on_hold: [\"placed\", \"paid\", \"processing\", \"cancelled\"],\n completed: [],\n cancelled: [],\n refunded: [],\n};\n\n/** Is moving from `from` to `to` a legal transition? */\nexport function canTransition(from: OrderStatus, to: OrderStatus): boolean {\n return ORDER_TRANSITIONS[from].includes(to);\n}\n\n/** Is `status` a terminal state (no further transitions possible)? */\nexport function isTerminal(status: OrderStatus): boolean {\n return ORDER_TRANSITIONS[status].length === 0;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Model */\n/* -------------------------------------------------------------------------- */\n\n/** A single, price-snapshotted line in the order. All money in minor units. */\nexport interface OrderLine {\n /** Stable identifier for this line. */\n id: string;\n /** Stock-keeping unit / catalogue reference. */\n sku: string;\n /** Human-readable name, snapshotted at order time. */\n name: string;\n /** Price per unit, in integer minor units, snapshotted at order time. */\n unitPrice: number;\n /** Quantity. */\n qty: number;\n /** Optional per-line tax rate in the range `0..1` (e.g. `0.2` = 20%). */\n taxRate?: number;\n /** Line total = `unitPrice * qty`, in integer minor units. */\n total: number;\n /** Arbitrary attached data (variant, image…). */\n meta?: Record<string, unknown>;\n}\n\n/** A timestamped entry in the order's status history. */\nexport interface StatusEvent {\n status: OrderStatus;\n /** Epoch milliseconds. */\n at: number;\n /** Optional human note describing the change. */\n note?: string;\n}\n\n/** Computed monetary totals for an order. All integer minor units. */\nexport interface OrderTotals {\n subtotal: number;\n discount: number;\n tax: number;\n shipping: number;\n total: number;\n}\n\n/** The order state. Plain data β€” safe to `JSON.stringify` and persist. */\nexport interface Order {\n /** Internal, opaque id. */\n id: string;\n /** Human-facing order number, e.g. `\"ORD-20260905-0001\"`. */\n number: string;\n status: OrderStatus;\n /** ISO-4217 currency code, e.g. `\"USD\"`. Purely informational. */\n currency: string;\n customer?: { id?: string; name?: string; email?: string };\n lines: OrderLine[];\n totals: OrderTotals;\n history: StatusEvent[];\n /** Epoch milliseconds. */\n createdAt: number;\n /** Epoch milliseconds. */\n updatedAt: number;\n meta?: Record<string, unknown>;\n}\n\n/** Error thrown by order operations. Carries an optional machine `code`. */\nexport class OrderError extends Error {\n code?: string;\n constructor(message: string, code?: string) {\n super(message);\n this.name = \"OrderError\";\n this.code = code;\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Convenience predicates (derived from ORDER_TRANSITIONS) */\n/* -------------------------------------------------------------------------- */\n\n/** Can this order still be cancelled from its current status? */\nexport function canCancel(order: Order): boolean {\n return canTransition(order.status, \"cancelled\");\n}\n\n/** Can this order still be refunded from its current status? */\nexport function canRefund(order: Order): boolean {\n return canTransition(order.status, \"refunded\");\n}\n\n/** Can this order transition to `shipped` from its current status? */\nexport function canShip(order: Order): boolean {\n return canTransition(order.status, \"shipped\");\n}\n\n/** Can this order transition to `fulfilled` from its current status? */\nexport function canFulfill(order: Order): boolean {\n return canTransition(order.status, \"fulfilled\");\n}\n\n/* -------------------------------------------------------------------------- */\n/* Internal helpers */\n/* -------------------------------------------------------------------------- */\n\n/** Input shape for a single line when building or amending an order. */\nexport interface OrderLineInput {\n id?: string;\n sku: string;\n name?: string;\n unitPrice: number;\n qty: number;\n taxRate?: number;\n meta?: Record<string, unknown>;\n}\n\nfunction snapshotLine(input: OrderLineInput): OrderLine {\n const qty = Math.max(0, Math.trunc(input.qty));\n const unitPrice = Math.trunc(input.unitPrice);\n const line: OrderLine = {\n id: input.id ?? input.sku,\n sku: input.sku,\n name: input.name ?? input.sku,\n unitPrice,\n qty,\n total: unitPrice * qty,\n };\n if (input.taxRate !== undefined) line.taxRate = input.taxRate;\n if (input.meta !== undefined) line.meta = input.meta;\n return line;\n}\n\nfunction computeTotals(\n lines: OrderLine[],\n opts: { discount?: number; shipping?: number; tax?: number },\n): OrderTotals {\n const subtotal = lines.reduce((sum, l) => sum + l.total, 0);\n const discount = Math.max(0, Math.trunc(opts.discount ?? 0));\n const shipping = Math.max(0, Math.trunc(opts.shipping ?? 0));\n const tax =\n opts.tax !== undefined\n ? Math.max(0, Math.trunc(opts.tax))\n : lines.reduce((sum, l) => sum + Math.round(l.total * (l.taxRate ?? 0)), 0);\n const total = Math.max(0, subtotal - discount + tax + shipping);\n return { subtotal, discount, tax, shipping, total };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Construction & transitions */\n/* -------------------------------------------------------------------------- */\n\n/** Input to {@link createOrder}. */\nexport interface CreateOrderInput {\n lines: OrderLineInput[];\n currency: string;\n customer?: { id?: string; name?: string; email?: string };\n /** Flat discount in integer minor units. */\n discount?: number;\n /** Flat shipping in integer minor units. */\n shipping?: number;\n /** Explicit tax override in minor units; otherwise derived per-line. */\n tax?: number;\n /** Initial status. Defaults to `\"pending\"`. */\n status?: OrderStatus;\n /** Epoch milliseconds for `createdAt`/first history event. Defaults to now. */\n now?: number;\n /** Opaque id. Defaults to a random id. */\n id?: string;\n /** Human-facing number. Defaults to the id. */\n number?: string;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Build a new, immutable `Order`. Each line is **snapshotted** (its `total` is\n * computed from `unitPrice * qty`) so later catalogue price changes never\n * mutate an existing order. Totals are computed and the history is seeded with\n * one `StatusEvent` for the initial status.\n */\nexport function createOrder(input: CreateOrderInput): Order {\n const now = input.now ?? Date.now();\n const lines = input.lines.map(snapshotLine);\n const totals = computeTotals(lines, input);\n const status = input.status ?? \"pending\";\n const id = input.id ?? randomOrderId({ prefix: \"ord\" });\n const order: Order = {\n id,\n number: input.number ?? id,\n status,\n currency: input.currency,\n lines,\n totals,\n history: [{ status, at: now }],\n createdAt: now,\n updatedAt: now,\n };\n if (input.customer !== undefined) order.customer = input.customer;\n if (input.meta !== undefined) order.meta = input.meta;\n return order;\n}\n\n/**\n * Transition an order to a new status. Validates via {@link canTransition} and\n * throws an `OrderError` (code `\"invalid-transition\"`) on an illegal move.\n * Returns a **new** order with the updated status, an appended history event\n * and a fresh `updatedAt`.\n */\nexport function transition(\n order: Order,\n to: OrderStatus,\n opts?: { at?: number; note?: string },\n): Order {\n if (!canTransition(order.status, to)) {\n throw new OrderError(\n `Cannot transition order from \"${order.status}\" to \"${to}\".`,\n \"invalid-transition\",\n );\n }\n const at = opts?.at ?? Date.now();\n const event: StatusEvent = { status: to, at };\n if (opts?.note !== undefined) event.note = opts.note;\n return {\n ...order,\n status: to,\n history: [...order.history, event],\n updatedAt: at,\n };\n}\n\n/** Statuses during which line edits are still permitted. */\nconst EDITABLE_STATUSES: OrderStatus[] = [\"pending\", \"placed\"];\n\nfunction assertEditable(order: Order): void {\n if (!EDITABLE_STATUSES.includes(order.status)) {\n throw new OrderError(\n `Order is locked for editing in status \"${order.status}\"; lines can only change while pending or placed.`,\n \"locked\",\n );\n }\n}\n\nfunction reprice(order: Order, lines: OrderLine[], now?: number): Order {\n const at = now ?? Date.now();\n return {\n ...order,\n lines,\n totals: computeTotals(lines, {\n discount: order.totals.discount,\n shipping: order.totals.shipping,\n }),\n updatedAt: at,\n };\n}\n\n/**\n * Add a line to a pre-payment order and recompute totals. Throws\n * `OrderError` (code `\"locked\"`) once the order is `paid` or beyond.\n */\nexport function addLine(order: Order, line: OrderLineInput, now?: number): Order {\n assertEditable(order);\n return reprice(order, [...order.lines, snapshotLine(line)], now);\n}\n\n/**\n * Remove a line by id from a pre-payment order and recompute totals. Throws\n * `OrderError` (code `\"locked\"`) once the order is `paid` or beyond.\n */\nexport function removeLine(order: Order, lineId: string, now?: number): Order {\n assertEditable(order);\n return reprice(\n order,\n order.lines.filter((l) => l.id !== lineId),\n now,\n );\n}\n\n/**\n * Set an absolute quantity for a line on a pre-payment order and recompute\n * totals. A `qty <= 0` removes the line. Throws `OrderError` (code `\"locked\"`)\n * once the order is `paid` or beyond.\n */\nexport function updateQty(\n order: Order,\n lineId: string,\n qty: number,\n now?: number,\n): Order {\n assertEditable(order);\n const next = Math.max(0, Math.trunc(qty));\n const lines = order.lines\n .map((l) =>\n l.id === lineId ? { ...l, qty: next, total: l.unitPrice * next } : l,\n )\n .filter((l) => l.qty > 0);\n return reprice(order, lines, now);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Numbering */\n/* -------------------------------------------------------------------------- */\n\nfunction pad(n: number, width: number): string {\n const s = String(Math.abs(Math.trunc(n)));\n return s.length >= width ? s : \"0\".repeat(width - s.length) + s;\n}\n\n/**\n * Build a deterministic, human-facing order number from a sequence.\n *\n * @example\n * orderNumber(1); // \"ORD-20260905-0001\" (with today's date)\n */\nexport function orderNumber(\n seq: number,\n opts?: { prefix?: string; date?: Date; pad?: number; separator?: string },\n): string {\n const prefix = opts?.prefix ?? \"ORD\";\n const date = opts?.date ?? new Date();\n const width = opts?.pad ?? 4;\n const sep = opts?.separator ?? \"-\";\n const y = date.getFullYear();\n const m = pad(date.getMonth() + 1, 2);\n const d = pad(date.getDate(), 2);\n return `${prefix}${sep}${y}${m}${d}${sep}${pad(seq, width)}`;\n}\n\nconst ID_ALPHABET = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\n/** Fill `out` with random bytes, preferring `crypto`, falling back to Math.random. */\nfunction randomBytes(out: Uint8Array): Uint8Array {\n const g: { crypto?: Crypto } =\n typeof globalThis !== \"undefined\" ? globalThis : {};\n if (g.crypto && typeof g.crypto.getRandomValues === \"function\") {\n g.crypto.getRandomValues(out);\n return out;\n }\n // Fallback: platform has no Web Crypto. Not cryptographically strong.\n if (typeof console !== \"undefined\" && typeof console.warn === \"function\") {\n console.warn(\n \"[@lacspace/order] globalThis.crypto is unavailable; falling back to Math.random for randomOrderId (not cryptographically secure).\",\n );\n }\n for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);\n return out;\n}\n\n/**\n * Generate a crypto-random short id, e.g. `\"ord_k3f9x1a7q2mz\"`. Uses\n * `globalThis.crypto.getRandomValues` when available, otherwise falls back to\n * `Math.random` with a warning.\n */\nexport function randomOrderId(opts?: { prefix?: string }): string {\n const bytes = randomBytes(new Uint8Array(16));\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) {\n out += ID_ALPHABET[bytes[i]! % ID_ALPHABET.length];\n }\n const prefix = opts?.prefix;\n return prefix ? `${prefix}_${out}` : out;\n}\n"]}
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @lacspace/order β€” a headless order-lifecycle engine.
3
+ *
4
+ * The spine between a cart and a courier: a pure, immutable and serializable
5
+ * `Order` model with a state machine, order-number generation, line-item price
6
+ * snapshotting, and a timestamped status history. Every operation returns a
7
+ * brand-new `Order`; the input is never mutated, so the state plays nicely with
8
+ * React, Redux, Zustand, signals, or a plain JSON column in your database.
9
+ *
10
+ * All money is expressed in **integer minor units** (cents, paise, satoshi…).
11
+ * There are no floats stored anywhere, so you never lose a penny to rounding.
12
+ *
13
+ * Isomorphic β€” no Node built-ins. The only platform API touched is
14
+ * `globalThis.crypto` for random ids, and that access is guarded.
15
+ */
16
+ /** Every status an order can hold across its lifecycle. */
17
+ type OrderStatus = "pending" | "placed" | "paid" | "processing" | "fulfilled" | "shipped" | "delivered" | "completed" | "on_hold" | "cancelled" | "refunded";
18
+ /**
19
+ * The allowed forward transitions for each status. Terminal statuses
20
+ * (`completed`, `cancelled`, `refunded`) map to an empty array.
21
+ */
22
+ declare const ORDER_TRANSITIONS: Record<OrderStatus, OrderStatus[]>;
23
+ /** Is moving from `from` to `to` a legal transition? */
24
+ declare function canTransition(from: OrderStatus, to: OrderStatus): boolean;
25
+ /** Is `status` a terminal state (no further transitions possible)? */
26
+ declare function isTerminal(status: OrderStatus): boolean;
27
+ /** A single, price-snapshotted line in the order. All money in minor units. */
28
+ interface OrderLine {
29
+ /** Stable identifier for this line. */
30
+ id: string;
31
+ /** Stock-keeping unit / catalogue reference. */
32
+ sku: string;
33
+ /** Human-readable name, snapshotted at order time. */
34
+ name: string;
35
+ /** Price per unit, in integer minor units, snapshotted at order time. */
36
+ unitPrice: number;
37
+ /** Quantity. */
38
+ qty: number;
39
+ /** Optional per-line tax rate in the range `0..1` (e.g. `0.2` = 20%). */
40
+ taxRate?: number;
41
+ /** Line total = `unitPrice * qty`, in integer minor units. */
42
+ total: number;
43
+ /** Arbitrary attached data (variant, image…). */
44
+ meta?: Record<string, unknown>;
45
+ }
46
+ /** A timestamped entry in the order's status history. */
47
+ interface StatusEvent {
48
+ status: OrderStatus;
49
+ /** Epoch milliseconds. */
50
+ at: number;
51
+ /** Optional human note describing the change. */
52
+ note?: string;
53
+ }
54
+ /** Computed monetary totals for an order. All integer minor units. */
55
+ interface OrderTotals {
56
+ subtotal: number;
57
+ discount: number;
58
+ tax: number;
59
+ shipping: number;
60
+ total: number;
61
+ }
62
+ /** The order state. Plain data β€” safe to `JSON.stringify` and persist. */
63
+ interface Order {
64
+ /** Internal, opaque id. */
65
+ id: string;
66
+ /** Human-facing order number, e.g. `"ORD-20260905-0001"`. */
67
+ number: string;
68
+ status: OrderStatus;
69
+ /** ISO-4217 currency code, e.g. `"USD"`. Purely informational. */
70
+ currency: string;
71
+ customer?: {
72
+ id?: string;
73
+ name?: string;
74
+ email?: string;
75
+ };
76
+ lines: OrderLine[];
77
+ totals: OrderTotals;
78
+ history: StatusEvent[];
79
+ /** Epoch milliseconds. */
80
+ createdAt: number;
81
+ /** Epoch milliseconds. */
82
+ updatedAt: number;
83
+ meta?: Record<string, unknown>;
84
+ }
85
+ /** Error thrown by order operations. Carries an optional machine `code`. */
86
+ declare class OrderError extends Error {
87
+ code?: string;
88
+ constructor(message: string, code?: string);
89
+ }
90
+ /** Can this order still be cancelled from its current status? */
91
+ declare function canCancel(order: Order): boolean;
92
+ /** Can this order still be refunded from its current status? */
93
+ declare function canRefund(order: Order): boolean;
94
+ /** Can this order transition to `shipped` from its current status? */
95
+ declare function canShip(order: Order): boolean;
96
+ /** Can this order transition to `fulfilled` from its current status? */
97
+ declare function canFulfill(order: Order): boolean;
98
+ /** Input shape for a single line when building or amending an order. */
99
+ interface OrderLineInput {
100
+ id?: string;
101
+ sku: string;
102
+ name?: string;
103
+ unitPrice: number;
104
+ qty: number;
105
+ taxRate?: number;
106
+ meta?: Record<string, unknown>;
107
+ }
108
+ /** Input to {@link createOrder}. */
109
+ interface CreateOrderInput {
110
+ lines: OrderLineInput[];
111
+ currency: string;
112
+ customer?: {
113
+ id?: string;
114
+ name?: string;
115
+ email?: string;
116
+ };
117
+ /** Flat discount in integer minor units. */
118
+ discount?: number;
119
+ /** Flat shipping in integer minor units. */
120
+ shipping?: number;
121
+ /** Explicit tax override in minor units; otherwise derived per-line. */
122
+ tax?: number;
123
+ /** Initial status. Defaults to `"pending"`. */
124
+ status?: OrderStatus;
125
+ /** Epoch milliseconds for `createdAt`/first history event. Defaults to now. */
126
+ now?: number;
127
+ /** Opaque id. Defaults to a random id. */
128
+ id?: string;
129
+ /** Human-facing number. Defaults to the id. */
130
+ number?: string;
131
+ meta?: Record<string, unknown>;
132
+ }
133
+ /**
134
+ * Build a new, immutable `Order`. Each line is **snapshotted** (its `total` is
135
+ * computed from `unitPrice * qty`) so later catalogue price changes never
136
+ * mutate an existing order. Totals are computed and the history is seeded with
137
+ * one `StatusEvent` for the initial status.
138
+ */
139
+ declare function createOrder(input: CreateOrderInput): Order;
140
+ /**
141
+ * Transition an order to a new status. Validates via {@link canTransition} and
142
+ * throws an `OrderError` (code `"invalid-transition"`) on an illegal move.
143
+ * Returns a **new** order with the updated status, an appended history event
144
+ * and a fresh `updatedAt`.
145
+ */
146
+ declare function transition(order: Order, to: OrderStatus, opts?: {
147
+ at?: number;
148
+ note?: string;
149
+ }): Order;
150
+ /**
151
+ * Add a line to a pre-payment order and recompute totals. Throws
152
+ * `OrderError` (code `"locked"`) once the order is `paid` or beyond.
153
+ */
154
+ declare function addLine(order: Order, line: OrderLineInput, now?: number): Order;
155
+ /**
156
+ * Remove a line by id from a pre-payment order and recompute totals. Throws
157
+ * `OrderError` (code `"locked"`) once the order is `paid` or beyond.
158
+ */
159
+ declare function removeLine(order: Order, lineId: string, now?: number): Order;
160
+ /**
161
+ * Set an absolute quantity for a line on a pre-payment order and recompute
162
+ * totals. A `qty <= 0` removes the line. Throws `OrderError` (code `"locked"`)
163
+ * once the order is `paid` or beyond.
164
+ */
165
+ declare function updateQty(order: Order, lineId: string, qty: number, now?: number): Order;
166
+ /**
167
+ * Build a deterministic, human-facing order number from a sequence.
168
+ *
169
+ * @example
170
+ * orderNumber(1); // "ORD-20260905-0001" (with today's date)
171
+ */
172
+ declare function orderNumber(seq: number, opts?: {
173
+ prefix?: string;
174
+ date?: Date;
175
+ pad?: number;
176
+ separator?: string;
177
+ }): string;
178
+ /**
179
+ * Generate a crypto-random short id, e.g. `"ord_k3f9x1a7q2mz"`. Uses
180
+ * `globalThis.crypto.getRandomValues` when available, otherwise falls back to
181
+ * `Math.random` with a warning.
182
+ */
183
+ declare function randomOrderId(opts?: {
184
+ prefix?: string;
185
+ }): string;
186
+
187
+ export { type CreateOrderInput, ORDER_TRANSITIONS, type Order, OrderError, type OrderLine, type OrderLineInput, type OrderStatus, type OrderTotals, type StatusEvent, addLine, canCancel, canFulfill, canRefund, canShip, canTransition, createOrder, isTerminal, orderNumber, randomOrderId, removeLine, transition, updateQty };
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @lacspace/order β€” a headless order-lifecycle engine.
3
+ *
4
+ * The spine between a cart and a courier: a pure, immutable and serializable
5
+ * `Order` model with a state machine, order-number generation, line-item price
6
+ * snapshotting, and a timestamped status history. Every operation returns a
7
+ * brand-new `Order`; the input is never mutated, so the state plays nicely with
8
+ * React, Redux, Zustand, signals, or a plain JSON column in your database.
9
+ *
10
+ * All money is expressed in **integer minor units** (cents, paise, satoshi…).
11
+ * There are no floats stored anywhere, so you never lose a penny to rounding.
12
+ *
13
+ * Isomorphic β€” no Node built-ins. The only platform API touched is
14
+ * `globalThis.crypto` for random ids, and that access is guarded.
15
+ */
16
+ /** Every status an order can hold across its lifecycle. */
17
+ type OrderStatus = "pending" | "placed" | "paid" | "processing" | "fulfilled" | "shipped" | "delivered" | "completed" | "on_hold" | "cancelled" | "refunded";
18
+ /**
19
+ * The allowed forward transitions for each status. Terminal statuses
20
+ * (`completed`, `cancelled`, `refunded`) map to an empty array.
21
+ */
22
+ declare const ORDER_TRANSITIONS: Record<OrderStatus, OrderStatus[]>;
23
+ /** Is moving from `from` to `to` a legal transition? */
24
+ declare function canTransition(from: OrderStatus, to: OrderStatus): boolean;
25
+ /** Is `status` a terminal state (no further transitions possible)? */
26
+ declare function isTerminal(status: OrderStatus): boolean;
27
+ /** A single, price-snapshotted line in the order. All money in minor units. */
28
+ interface OrderLine {
29
+ /** Stable identifier for this line. */
30
+ id: string;
31
+ /** Stock-keeping unit / catalogue reference. */
32
+ sku: string;
33
+ /** Human-readable name, snapshotted at order time. */
34
+ name: string;
35
+ /** Price per unit, in integer minor units, snapshotted at order time. */
36
+ unitPrice: number;
37
+ /** Quantity. */
38
+ qty: number;
39
+ /** Optional per-line tax rate in the range `0..1` (e.g. `0.2` = 20%). */
40
+ taxRate?: number;
41
+ /** Line total = `unitPrice * qty`, in integer minor units. */
42
+ total: number;
43
+ /** Arbitrary attached data (variant, image…). */
44
+ meta?: Record<string, unknown>;
45
+ }
46
+ /** A timestamped entry in the order's status history. */
47
+ interface StatusEvent {
48
+ status: OrderStatus;
49
+ /** Epoch milliseconds. */
50
+ at: number;
51
+ /** Optional human note describing the change. */
52
+ note?: string;
53
+ }
54
+ /** Computed monetary totals for an order. All integer minor units. */
55
+ interface OrderTotals {
56
+ subtotal: number;
57
+ discount: number;
58
+ tax: number;
59
+ shipping: number;
60
+ total: number;
61
+ }
62
+ /** The order state. Plain data β€” safe to `JSON.stringify` and persist. */
63
+ interface Order {
64
+ /** Internal, opaque id. */
65
+ id: string;
66
+ /** Human-facing order number, e.g. `"ORD-20260905-0001"`. */
67
+ number: string;
68
+ status: OrderStatus;
69
+ /** ISO-4217 currency code, e.g. `"USD"`. Purely informational. */
70
+ currency: string;
71
+ customer?: {
72
+ id?: string;
73
+ name?: string;
74
+ email?: string;
75
+ };
76
+ lines: OrderLine[];
77
+ totals: OrderTotals;
78
+ history: StatusEvent[];
79
+ /** Epoch milliseconds. */
80
+ createdAt: number;
81
+ /** Epoch milliseconds. */
82
+ updatedAt: number;
83
+ meta?: Record<string, unknown>;
84
+ }
85
+ /** Error thrown by order operations. Carries an optional machine `code`. */
86
+ declare class OrderError extends Error {
87
+ code?: string;
88
+ constructor(message: string, code?: string);
89
+ }
90
+ /** Can this order still be cancelled from its current status? */
91
+ declare function canCancel(order: Order): boolean;
92
+ /** Can this order still be refunded from its current status? */
93
+ declare function canRefund(order: Order): boolean;
94
+ /** Can this order transition to `shipped` from its current status? */
95
+ declare function canShip(order: Order): boolean;
96
+ /** Can this order transition to `fulfilled` from its current status? */
97
+ declare function canFulfill(order: Order): boolean;
98
+ /** Input shape for a single line when building or amending an order. */
99
+ interface OrderLineInput {
100
+ id?: string;
101
+ sku: string;
102
+ name?: string;
103
+ unitPrice: number;
104
+ qty: number;
105
+ taxRate?: number;
106
+ meta?: Record<string, unknown>;
107
+ }
108
+ /** Input to {@link createOrder}. */
109
+ interface CreateOrderInput {
110
+ lines: OrderLineInput[];
111
+ currency: string;
112
+ customer?: {
113
+ id?: string;
114
+ name?: string;
115
+ email?: string;
116
+ };
117
+ /** Flat discount in integer minor units. */
118
+ discount?: number;
119
+ /** Flat shipping in integer minor units. */
120
+ shipping?: number;
121
+ /** Explicit tax override in minor units; otherwise derived per-line. */
122
+ tax?: number;
123
+ /** Initial status. Defaults to `"pending"`. */
124
+ status?: OrderStatus;
125
+ /** Epoch milliseconds for `createdAt`/first history event. Defaults to now. */
126
+ now?: number;
127
+ /** Opaque id. Defaults to a random id. */
128
+ id?: string;
129
+ /** Human-facing number. Defaults to the id. */
130
+ number?: string;
131
+ meta?: Record<string, unknown>;
132
+ }
133
+ /**
134
+ * Build a new, immutable `Order`. Each line is **snapshotted** (its `total` is
135
+ * computed from `unitPrice * qty`) so later catalogue price changes never
136
+ * mutate an existing order. Totals are computed and the history is seeded with
137
+ * one `StatusEvent` for the initial status.
138
+ */
139
+ declare function createOrder(input: CreateOrderInput): Order;
140
+ /**
141
+ * Transition an order to a new status. Validates via {@link canTransition} and
142
+ * throws an `OrderError` (code `"invalid-transition"`) on an illegal move.
143
+ * Returns a **new** order with the updated status, an appended history event
144
+ * and a fresh `updatedAt`.
145
+ */
146
+ declare function transition(order: Order, to: OrderStatus, opts?: {
147
+ at?: number;
148
+ note?: string;
149
+ }): Order;
150
+ /**
151
+ * Add a line to a pre-payment order and recompute totals. Throws
152
+ * `OrderError` (code `"locked"`) once the order is `paid` or beyond.
153
+ */
154
+ declare function addLine(order: Order, line: OrderLineInput, now?: number): Order;
155
+ /**
156
+ * Remove a line by id from a pre-payment order and recompute totals. Throws
157
+ * `OrderError` (code `"locked"`) once the order is `paid` or beyond.
158
+ */
159
+ declare function removeLine(order: Order, lineId: string, now?: number): Order;
160
+ /**
161
+ * Set an absolute quantity for a line on a pre-payment order and recompute
162
+ * totals. A `qty <= 0` removes the line. Throws `OrderError` (code `"locked"`)
163
+ * once the order is `paid` or beyond.
164
+ */
165
+ declare function updateQty(order: Order, lineId: string, qty: number, now?: number): Order;
166
+ /**
167
+ * Build a deterministic, human-facing order number from a sequence.
168
+ *
169
+ * @example
170
+ * orderNumber(1); // "ORD-20260905-0001" (with today's date)
171
+ */
172
+ declare function orderNumber(seq: number, opts?: {
173
+ prefix?: string;
174
+ date?: Date;
175
+ pad?: number;
176
+ separator?: string;
177
+ }): string;
178
+ /**
179
+ * Generate a crypto-random short id, e.g. `"ord_k3f9x1a7q2mz"`. Uses
180
+ * `globalThis.crypto.getRandomValues` when available, otherwise falls back to
181
+ * `Math.random` with a warning.
182
+ */
183
+ declare function randomOrderId(opts?: {
184
+ prefix?: string;
185
+ }): string;
186
+
187
+ export { type CreateOrderInput, ORDER_TRANSITIONS, type Order, OrderError, type OrderLine, type OrderLineInput, type OrderStatus, type OrderTotals, type StatusEvent, addLine, canCancel, canFulfill, canRefund, canShip, canTransition, createOrder, isTerminal, orderNumber, randomOrderId, removeLine, transition, updateQty };
package/dist/index.js ADDED
@@ -0,0 +1,183 @@
1
+ // src/index.ts
2
+ var ORDER_TRANSITIONS = {
3
+ pending: ["placed", "cancelled"],
4
+ placed: ["paid", "cancelled", "on_hold"],
5
+ paid: ["processing", "refunded", "on_hold", "cancelled"],
6
+ processing: ["fulfilled", "on_hold", "cancelled", "refunded"],
7
+ fulfilled: ["shipped", "refunded"],
8
+ shipped: ["delivered", "refunded"],
9
+ delivered: ["completed", "refunded"],
10
+ on_hold: ["placed", "paid", "processing", "cancelled"],
11
+ completed: [],
12
+ cancelled: [],
13
+ refunded: []
14
+ };
15
+ function canTransition(from, to) {
16
+ return ORDER_TRANSITIONS[from].includes(to);
17
+ }
18
+ function isTerminal(status) {
19
+ return ORDER_TRANSITIONS[status].length === 0;
20
+ }
21
+ var OrderError = class extends Error {
22
+ constructor(message, code) {
23
+ super(message);
24
+ this.name = "OrderError";
25
+ this.code = code;
26
+ }
27
+ };
28
+ function canCancel(order) {
29
+ return canTransition(order.status, "cancelled");
30
+ }
31
+ function canRefund(order) {
32
+ return canTransition(order.status, "refunded");
33
+ }
34
+ function canShip(order) {
35
+ return canTransition(order.status, "shipped");
36
+ }
37
+ function canFulfill(order) {
38
+ return canTransition(order.status, "fulfilled");
39
+ }
40
+ function snapshotLine(input) {
41
+ const qty = Math.max(0, Math.trunc(input.qty));
42
+ const unitPrice = Math.trunc(input.unitPrice);
43
+ const line = {
44
+ id: input.id ?? input.sku,
45
+ sku: input.sku,
46
+ name: input.name ?? input.sku,
47
+ unitPrice,
48
+ qty,
49
+ total: unitPrice * qty
50
+ };
51
+ if (input.taxRate !== void 0) line.taxRate = input.taxRate;
52
+ if (input.meta !== void 0) line.meta = input.meta;
53
+ return line;
54
+ }
55
+ function computeTotals(lines, opts) {
56
+ const subtotal = lines.reduce((sum, l) => sum + l.total, 0);
57
+ const discount = Math.max(0, Math.trunc(opts.discount ?? 0));
58
+ const shipping = Math.max(0, Math.trunc(opts.shipping ?? 0));
59
+ const tax = opts.tax !== void 0 ? Math.max(0, Math.trunc(opts.tax)) : lines.reduce((sum, l) => sum + Math.round(l.total * (l.taxRate ?? 0)), 0);
60
+ const total = Math.max(0, subtotal - discount + tax + shipping);
61
+ return { subtotal, discount, tax, shipping, total };
62
+ }
63
+ function createOrder(input) {
64
+ const now = input.now ?? Date.now();
65
+ const lines = input.lines.map(snapshotLine);
66
+ const totals = computeTotals(lines, input);
67
+ const status = input.status ?? "pending";
68
+ const id = input.id ?? randomOrderId({ prefix: "ord" });
69
+ const order = {
70
+ id,
71
+ number: input.number ?? id,
72
+ status,
73
+ currency: input.currency,
74
+ lines,
75
+ totals,
76
+ history: [{ status, at: now }],
77
+ createdAt: now,
78
+ updatedAt: now
79
+ };
80
+ if (input.customer !== void 0) order.customer = input.customer;
81
+ if (input.meta !== void 0) order.meta = input.meta;
82
+ return order;
83
+ }
84
+ function transition(order, to, opts) {
85
+ if (!canTransition(order.status, to)) {
86
+ throw new OrderError(
87
+ `Cannot transition order from "${order.status}" to "${to}".`,
88
+ "invalid-transition"
89
+ );
90
+ }
91
+ const at = opts?.at ?? Date.now();
92
+ const event = { status: to, at };
93
+ if (opts?.note !== void 0) event.note = opts.note;
94
+ return {
95
+ ...order,
96
+ status: to,
97
+ history: [...order.history, event],
98
+ updatedAt: at
99
+ };
100
+ }
101
+ var EDITABLE_STATUSES = ["pending", "placed"];
102
+ function assertEditable(order) {
103
+ if (!EDITABLE_STATUSES.includes(order.status)) {
104
+ throw new OrderError(
105
+ `Order is locked for editing in status "${order.status}"; lines can only change while pending or placed.`,
106
+ "locked"
107
+ );
108
+ }
109
+ }
110
+ function reprice(order, lines, now) {
111
+ const at = now ?? Date.now();
112
+ return {
113
+ ...order,
114
+ lines,
115
+ totals: computeTotals(lines, {
116
+ discount: order.totals.discount,
117
+ shipping: order.totals.shipping
118
+ }),
119
+ updatedAt: at
120
+ };
121
+ }
122
+ function addLine(order, line, now) {
123
+ assertEditable(order);
124
+ return reprice(order, [...order.lines, snapshotLine(line)], now);
125
+ }
126
+ function removeLine(order, lineId, now) {
127
+ assertEditable(order);
128
+ return reprice(
129
+ order,
130
+ order.lines.filter((l) => l.id !== lineId),
131
+ now
132
+ );
133
+ }
134
+ function updateQty(order, lineId, qty, now) {
135
+ assertEditable(order);
136
+ const next = Math.max(0, Math.trunc(qty));
137
+ const lines = order.lines.map(
138
+ (l) => l.id === lineId ? { ...l, qty: next, total: l.unitPrice * next } : l
139
+ ).filter((l) => l.qty > 0);
140
+ return reprice(order, lines, now);
141
+ }
142
+ function pad(n, width) {
143
+ const s = String(Math.abs(Math.trunc(n)));
144
+ return s.length >= width ? s : "0".repeat(width - s.length) + s;
145
+ }
146
+ function orderNumber(seq, opts) {
147
+ const prefix = opts?.prefix ?? "ORD";
148
+ const date = opts?.date ?? /* @__PURE__ */ new Date();
149
+ const width = opts?.pad ?? 4;
150
+ const sep = opts?.separator ?? "-";
151
+ const y = date.getFullYear();
152
+ const m = pad(date.getMonth() + 1, 2);
153
+ const d = pad(date.getDate(), 2);
154
+ return `${prefix}${sep}${y}${m}${d}${sep}${pad(seq, width)}`;
155
+ }
156
+ var ID_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
157
+ function randomBytes(out) {
158
+ const g = typeof globalThis !== "undefined" ? globalThis : {};
159
+ if (g.crypto && typeof g.crypto.getRandomValues === "function") {
160
+ g.crypto.getRandomValues(out);
161
+ return out;
162
+ }
163
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
164
+ console.warn(
165
+ "[@lacspace/order] globalThis.crypto is unavailable; falling back to Math.random for randomOrderId (not cryptographically secure)."
166
+ );
167
+ }
168
+ for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);
169
+ return out;
170
+ }
171
+ function randomOrderId(opts) {
172
+ const bytes = randomBytes(new Uint8Array(16));
173
+ let out = "";
174
+ for (let i = 0; i < bytes.length; i++) {
175
+ out += ID_ALPHABET[bytes[i] % ID_ALPHABET.length];
176
+ }
177
+ const prefix = opts?.prefix;
178
+ return prefix ? `${prefix}_${out}` : out;
179
+ }
180
+
181
+ export { ORDER_TRANSITIONS, OrderError, addLine, canCancel, canFulfill, canRefund, canShip, canTransition, createOrder, isTerminal, orderNumber, randomOrderId, removeLine, transition, updateQty };
182
+ //# sourceMappingURL=index.js.map
183
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/order β€” a headless order-lifecycle engine.\n *\n * The spine between a cart and a courier: a pure, immutable and serializable\n * `Order` model with a state machine, order-number generation, line-item price\n * snapshotting, and a timestamped status history. Every operation returns a\n * brand-new `Order`; the input is never mutated, so the state plays nicely with\n * React, Redux, Zustand, signals, or a plain JSON column in your database.\n *\n * All money is expressed in **integer minor units** (cents, paise, satoshi…).\n * There are no floats stored anywhere, so you never lose a penny to rounding.\n *\n * Isomorphic β€” no Node built-ins. The only platform API touched is\n * `globalThis.crypto` for random ids, and that access is guarded.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Status + state machine */\n/* -------------------------------------------------------------------------- */\n\n/** Every status an order can hold across its lifecycle. */\nexport type OrderStatus =\n | \"pending\"\n | \"placed\"\n | \"paid\"\n | \"processing\"\n | \"fulfilled\"\n | \"shipped\"\n | \"delivered\"\n | \"completed\"\n | \"on_hold\"\n | \"cancelled\"\n | \"refunded\";\n\n/**\n * The allowed forward transitions for each status. Terminal statuses\n * (`completed`, `cancelled`, `refunded`) map to an empty array.\n */\nexport const ORDER_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {\n pending: [\"placed\", \"cancelled\"],\n placed: [\"paid\", \"cancelled\", \"on_hold\"],\n paid: [\"processing\", \"refunded\", \"on_hold\", \"cancelled\"],\n processing: [\"fulfilled\", \"on_hold\", \"cancelled\", \"refunded\"],\n fulfilled: [\"shipped\", \"refunded\"],\n shipped: [\"delivered\", \"refunded\"],\n delivered: [\"completed\", \"refunded\"],\n on_hold: [\"placed\", \"paid\", \"processing\", \"cancelled\"],\n completed: [],\n cancelled: [],\n refunded: [],\n};\n\n/** Is moving from `from` to `to` a legal transition? */\nexport function canTransition(from: OrderStatus, to: OrderStatus): boolean {\n return ORDER_TRANSITIONS[from].includes(to);\n}\n\n/** Is `status` a terminal state (no further transitions possible)? */\nexport function isTerminal(status: OrderStatus): boolean {\n return ORDER_TRANSITIONS[status].length === 0;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Model */\n/* -------------------------------------------------------------------------- */\n\n/** A single, price-snapshotted line in the order. All money in minor units. */\nexport interface OrderLine {\n /** Stable identifier for this line. */\n id: string;\n /** Stock-keeping unit / catalogue reference. */\n sku: string;\n /** Human-readable name, snapshotted at order time. */\n name: string;\n /** Price per unit, in integer minor units, snapshotted at order time. */\n unitPrice: number;\n /** Quantity. */\n qty: number;\n /** Optional per-line tax rate in the range `0..1` (e.g. `0.2` = 20%). */\n taxRate?: number;\n /** Line total = `unitPrice * qty`, in integer minor units. */\n total: number;\n /** Arbitrary attached data (variant, image…). */\n meta?: Record<string, unknown>;\n}\n\n/** A timestamped entry in the order's status history. */\nexport interface StatusEvent {\n status: OrderStatus;\n /** Epoch milliseconds. */\n at: number;\n /** Optional human note describing the change. */\n note?: string;\n}\n\n/** Computed monetary totals for an order. All integer minor units. */\nexport interface OrderTotals {\n subtotal: number;\n discount: number;\n tax: number;\n shipping: number;\n total: number;\n}\n\n/** The order state. Plain data β€” safe to `JSON.stringify` and persist. */\nexport interface Order {\n /** Internal, opaque id. */\n id: string;\n /** Human-facing order number, e.g. `\"ORD-20260905-0001\"`. */\n number: string;\n status: OrderStatus;\n /** ISO-4217 currency code, e.g. `\"USD\"`. Purely informational. */\n currency: string;\n customer?: { id?: string; name?: string; email?: string };\n lines: OrderLine[];\n totals: OrderTotals;\n history: StatusEvent[];\n /** Epoch milliseconds. */\n createdAt: number;\n /** Epoch milliseconds. */\n updatedAt: number;\n meta?: Record<string, unknown>;\n}\n\n/** Error thrown by order operations. Carries an optional machine `code`. */\nexport class OrderError extends Error {\n code?: string;\n constructor(message: string, code?: string) {\n super(message);\n this.name = \"OrderError\";\n this.code = code;\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Convenience predicates (derived from ORDER_TRANSITIONS) */\n/* -------------------------------------------------------------------------- */\n\n/** Can this order still be cancelled from its current status? */\nexport function canCancel(order: Order): boolean {\n return canTransition(order.status, \"cancelled\");\n}\n\n/** Can this order still be refunded from its current status? */\nexport function canRefund(order: Order): boolean {\n return canTransition(order.status, \"refunded\");\n}\n\n/** Can this order transition to `shipped` from its current status? */\nexport function canShip(order: Order): boolean {\n return canTransition(order.status, \"shipped\");\n}\n\n/** Can this order transition to `fulfilled` from its current status? */\nexport function canFulfill(order: Order): boolean {\n return canTransition(order.status, \"fulfilled\");\n}\n\n/* -------------------------------------------------------------------------- */\n/* Internal helpers */\n/* -------------------------------------------------------------------------- */\n\n/** Input shape for a single line when building or amending an order. */\nexport interface OrderLineInput {\n id?: string;\n sku: string;\n name?: string;\n unitPrice: number;\n qty: number;\n taxRate?: number;\n meta?: Record<string, unknown>;\n}\n\nfunction snapshotLine(input: OrderLineInput): OrderLine {\n const qty = Math.max(0, Math.trunc(input.qty));\n const unitPrice = Math.trunc(input.unitPrice);\n const line: OrderLine = {\n id: input.id ?? input.sku,\n sku: input.sku,\n name: input.name ?? input.sku,\n unitPrice,\n qty,\n total: unitPrice * qty,\n };\n if (input.taxRate !== undefined) line.taxRate = input.taxRate;\n if (input.meta !== undefined) line.meta = input.meta;\n return line;\n}\n\nfunction computeTotals(\n lines: OrderLine[],\n opts: { discount?: number; shipping?: number; tax?: number },\n): OrderTotals {\n const subtotal = lines.reduce((sum, l) => sum + l.total, 0);\n const discount = Math.max(0, Math.trunc(opts.discount ?? 0));\n const shipping = Math.max(0, Math.trunc(opts.shipping ?? 0));\n const tax =\n opts.tax !== undefined\n ? Math.max(0, Math.trunc(opts.tax))\n : lines.reduce((sum, l) => sum + Math.round(l.total * (l.taxRate ?? 0)), 0);\n const total = Math.max(0, subtotal - discount + tax + shipping);\n return { subtotal, discount, tax, shipping, total };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Construction & transitions */\n/* -------------------------------------------------------------------------- */\n\n/** Input to {@link createOrder}. */\nexport interface CreateOrderInput {\n lines: OrderLineInput[];\n currency: string;\n customer?: { id?: string; name?: string; email?: string };\n /** Flat discount in integer minor units. */\n discount?: number;\n /** Flat shipping in integer minor units. */\n shipping?: number;\n /** Explicit tax override in minor units; otherwise derived per-line. */\n tax?: number;\n /** Initial status. Defaults to `\"pending\"`. */\n status?: OrderStatus;\n /** Epoch milliseconds for `createdAt`/first history event. Defaults to now. */\n now?: number;\n /** Opaque id. Defaults to a random id. */\n id?: string;\n /** Human-facing number. Defaults to the id. */\n number?: string;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Build a new, immutable `Order`. Each line is **snapshotted** (its `total` is\n * computed from `unitPrice * qty`) so later catalogue price changes never\n * mutate an existing order. Totals are computed and the history is seeded with\n * one `StatusEvent` for the initial status.\n */\nexport function createOrder(input: CreateOrderInput): Order {\n const now = input.now ?? Date.now();\n const lines = input.lines.map(snapshotLine);\n const totals = computeTotals(lines, input);\n const status = input.status ?? \"pending\";\n const id = input.id ?? randomOrderId({ prefix: \"ord\" });\n const order: Order = {\n id,\n number: input.number ?? id,\n status,\n currency: input.currency,\n lines,\n totals,\n history: [{ status, at: now }],\n createdAt: now,\n updatedAt: now,\n };\n if (input.customer !== undefined) order.customer = input.customer;\n if (input.meta !== undefined) order.meta = input.meta;\n return order;\n}\n\n/**\n * Transition an order to a new status. Validates via {@link canTransition} and\n * throws an `OrderError` (code `\"invalid-transition\"`) on an illegal move.\n * Returns a **new** order with the updated status, an appended history event\n * and a fresh `updatedAt`.\n */\nexport function transition(\n order: Order,\n to: OrderStatus,\n opts?: { at?: number; note?: string },\n): Order {\n if (!canTransition(order.status, to)) {\n throw new OrderError(\n `Cannot transition order from \"${order.status}\" to \"${to}\".`,\n \"invalid-transition\",\n );\n }\n const at = opts?.at ?? Date.now();\n const event: StatusEvent = { status: to, at };\n if (opts?.note !== undefined) event.note = opts.note;\n return {\n ...order,\n status: to,\n history: [...order.history, event],\n updatedAt: at,\n };\n}\n\n/** Statuses during which line edits are still permitted. */\nconst EDITABLE_STATUSES: OrderStatus[] = [\"pending\", \"placed\"];\n\nfunction assertEditable(order: Order): void {\n if (!EDITABLE_STATUSES.includes(order.status)) {\n throw new OrderError(\n `Order is locked for editing in status \"${order.status}\"; lines can only change while pending or placed.`,\n \"locked\",\n );\n }\n}\n\nfunction reprice(order: Order, lines: OrderLine[], now?: number): Order {\n const at = now ?? Date.now();\n return {\n ...order,\n lines,\n totals: computeTotals(lines, {\n discount: order.totals.discount,\n shipping: order.totals.shipping,\n }),\n updatedAt: at,\n };\n}\n\n/**\n * Add a line to a pre-payment order and recompute totals. Throws\n * `OrderError` (code `\"locked\"`) once the order is `paid` or beyond.\n */\nexport function addLine(order: Order, line: OrderLineInput, now?: number): Order {\n assertEditable(order);\n return reprice(order, [...order.lines, snapshotLine(line)], now);\n}\n\n/**\n * Remove a line by id from a pre-payment order and recompute totals. Throws\n * `OrderError` (code `\"locked\"`) once the order is `paid` or beyond.\n */\nexport function removeLine(order: Order, lineId: string, now?: number): Order {\n assertEditable(order);\n return reprice(\n order,\n order.lines.filter((l) => l.id !== lineId),\n now,\n );\n}\n\n/**\n * Set an absolute quantity for a line on a pre-payment order and recompute\n * totals. A `qty <= 0` removes the line. Throws `OrderError` (code `\"locked\"`)\n * once the order is `paid` or beyond.\n */\nexport function updateQty(\n order: Order,\n lineId: string,\n qty: number,\n now?: number,\n): Order {\n assertEditable(order);\n const next = Math.max(0, Math.trunc(qty));\n const lines = order.lines\n .map((l) =>\n l.id === lineId ? { ...l, qty: next, total: l.unitPrice * next } : l,\n )\n .filter((l) => l.qty > 0);\n return reprice(order, lines, now);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Numbering */\n/* -------------------------------------------------------------------------- */\n\nfunction pad(n: number, width: number): string {\n const s = String(Math.abs(Math.trunc(n)));\n return s.length >= width ? s : \"0\".repeat(width - s.length) + s;\n}\n\n/**\n * Build a deterministic, human-facing order number from a sequence.\n *\n * @example\n * orderNumber(1); // \"ORD-20260905-0001\" (with today's date)\n */\nexport function orderNumber(\n seq: number,\n opts?: { prefix?: string; date?: Date; pad?: number; separator?: string },\n): string {\n const prefix = opts?.prefix ?? \"ORD\";\n const date = opts?.date ?? new Date();\n const width = opts?.pad ?? 4;\n const sep = opts?.separator ?? \"-\";\n const y = date.getFullYear();\n const m = pad(date.getMonth() + 1, 2);\n const d = pad(date.getDate(), 2);\n return `${prefix}${sep}${y}${m}${d}${sep}${pad(seq, width)}`;\n}\n\nconst ID_ALPHABET = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\n/** Fill `out` with random bytes, preferring `crypto`, falling back to Math.random. */\nfunction randomBytes(out: Uint8Array): Uint8Array {\n const g: { crypto?: Crypto } =\n typeof globalThis !== \"undefined\" ? globalThis : {};\n if (g.crypto && typeof g.crypto.getRandomValues === \"function\") {\n g.crypto.getRandomValues(out);\n return out;\n }\n // Fallback: platform has no Web Crypto. Not cryptographically strong.\n if (typeof console !== \"undefined\" && typeof console.warn === \"function\") {\n console.warn(\n \"[@lacspace/order] globalThis.crypto is unavailable; falling back to Math.random for randomOrderId (not cryptographically secure).\",\n );\n }\n for (let i = 0; i < out.length; i++) out[i] = Math.floor(Math.random() * 256);\n return out;\n}\n\n/**\n * Generate a crypto-random short id, e.g. `\"ord_k3f9x1a7q2mz\"`. Uses\n * `globalThis.crypto.getRandomValues` when available, otherwise falls back to\n * `Math.random` with a warning.\n */\nexport function randomOrderId(opts?: { prefix?: string }): string {\n const bytes = randomBytes(new Uint8Array(16));\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) {\n out += ID_ALPHABET[bytes[i]! % ID_ALPHABET.length];\n }\n const prefix = opts?.prefix;\n return prefix ? `${prefix}_${out}` : out;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@lacspace/order",
3
+ "version": "1.0.0",
4
+ "description": "Headless order-lifecycle engine β€” an immutable order model with a state machine, order-number generation, line-item price snapshotting and timestamped status history. Integer minor units, zero-dependency, isomorphic (Node, edge, browser).",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "order",
31
+ "order-management",
32
+ "ecommerce",
33
+ "state-machine",
34
+ "order-status",
35
+ "checkout",
36
+ "fulfillment",
37
+ "minor-units",
38
+ "headless",
39
+ "immutable",
40
+ "isomorphic",
41
+ "typescript"
42
+ ],
43
+ "author": "Lacspace <contact@lacspace.com>",
44
+ "license": "SEE LICENSE IN LICENSE",
45
+ "homepage": "https://developer.lacspace.com/packages/order",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/lacspace/npm-packages.git",
49
+ "directory": "order"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/lacspace/npm-packages/issues"
53
+ },
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "dependencies": {},
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }