@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 +51 -0
- package/README.md +130 -0
- package/dist/index.cjs +199 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
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
|
+
[](https://www.npmjs.com/package/@lacspace/order)
|
|
8
|
+
[](https://bundlephobia.com/package/@lacspace/order)
|
|
9
|
+
[](https://www.npmjs.com/package/@lacspace/order)
|
|
10
|
+
[](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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|