@lacspace/inventory 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 +105 -0
- package/dist/index.cjs +77 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +66 -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,105 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @lacspace/inventory
|
|
4
|
+
|
|
5
|
+
**A stock-tracking engine that prevents overselling โ reserve, commit & restock over a plain state object.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@lacspace/inventory)
|
|
8
|
+
[](https://bundlephobia.com/package/@lacspace/inventory)
|
|
9
|
+
[](https://www.npmjs.com/package/@lacspace/inventory)
|
|
10
|
+
[](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
> Overselling is one bug: you let two orders take the last unit. This is the maths that stops it โ pure, immutable functions over a plain `{ onHand, reserved }` state. **Bring your own store** (a DB row, a cache, a signal); this decides what's allowed.
|
|
15
|
+
|
|
16
|
+
- ๐ซ **No overselling** โ `reserve`/`commit` throw an `InventoryError` before they'd go negative
|
|
17
|
+
- ๐ง **Immutable** โ every op returns a new `Stock`; your input is never mutated
|
|
18
|
+
- ๐ง **available = onHand โ reserved** โ the one invariant, enforced everywhere
|
|
19
|
+
- ๐งฉ **Headless** โ persist the state however you like; this is just the rules
|
|
20
|
+
- โก Isomorphic โ Node, edge runtimes & browsers ยท ๐ฆ ESM + CJS ยท fully typed ยท zero deps
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i @lacspace/inventory # or pnpm add / yarn add / bun add
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Reserve โ commit
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createStock, reserve, commit, available } from "@lacspace/inventory";
|
|
32
|
+
|
|
33
|
+
let stock = createStock(10); // { onHand: 10, reserved: 0 }
|
|
34
|
+
|
|
35
|
+
stock = reserve(stock, 3); // hold 3 for a pending order
|
|
36
|
+
available(stock); // 7
|
|
37
|
+
|
|
38
|
+
stock = commit(stock, 3); // ship them
|
|
39
|
+
stock; // { onHand: 7, reserved: 0 }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Overselling throws
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createStock, reserve, InventoryError } from "@lacspace/inventory";
|
|
46
|
+
|
|
47
|
+
const stock = createStock(2);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
reserve(stock, 5); // only 2 available
|
|
51
|
+
} catch (e) {
|
|
52
|
+
e instanceof InventoryError; // true โ nothing was oversold
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Restock, release & low-stock alerts
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createStock, reserve, release, restock, isLow, isOutOfStock } from "@lacspace/inventory";
|
|
60
|
+
|
|
61
|
+
let stock = restock(createStock(0), 20); // delivery arrives
|
|
62
|
+
stock = reserve(stock, 18);
|
|
63
|
+
|
|
64
|
+
isLow(stock, 5); // true โ 2 available
|
|
65
|
+
isOutOfStock(stock); // false
|
|
66
|
+
|
|
67
|
+
stock = release(stock, 18); // cart abandoned โ put them back
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
| Function | Description |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `createStock(onHand?)` | new `{ onHand, reserved: 0 }` (default `0`) |
|
|
75
|
+
| `available(stock)` | `onHand - reserved` |
|
|
76
|
+
| `reserve(stock, qty)` | hold `qty`; **throws** if `qty > available` |
|
|
77
|
+
| `release(stock, qty)` | free a reservation (never below `0`) |
|
|
78
|
+
| `commit(stock, qty)` | fulfil: `onHand -= qty`, `reserved -= qty`; **throws** if `qty > reserved` |
|
|
79
|
+
| `restock(stock, qty)` | add `qty` to `onHand` |
|
|
80
|
+
| `adjust(stock, delta)` | signed correction to `onHand` (clamped at `0`) |
|
|
81
|
+
| `isLow(stock, threshold)` | `available <= threshold` |
|
|
82
|
+
| `isOutOfStock(stock)` | `available <= 0` |
|
|
83
|
+
| `InventoryError` | thrown on oversell / invalid quantity |
|
|
84
|
+
|
|
85
|
+
## Licensing
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
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)**.
|
|
90
|
+
|
|
91
|
+
<!-- LACSPACE-DEV-PLATFORM -->
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## The Lacspace Developer Platform
|
|
96
|
+
|
|
97
|
+
`@lacspace/inventory` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
|
|
98
|
+
|
|
99
|
+
- ๐๏ธ **All packages** โ https://developer.lacspace.com/packages
|
|
100
|
+
- ๐งญ **Developer handbook** โ https://developer.lacspace.com/handbook
|
|
101
|
+
- ๐งช **Live playground** โ https://developer.lacspace.com/playground
|
|
102
|
+
- ๐ฅ๏ธ **Finished app templates** โ https://templates.lacspace.com
|
|
103
|
+
- ๐ **Scaffold a full app** โ `npm create lacspace-app@latest`
|
|
104
|
+
|
|
105
|
+
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,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var InventoryError = class _InventoryError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "InventoryError";
|
|
8
|
+
Object.setPrototypeOf(this, _InventoryError.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function toCount(n, label) {
|
|
12
|
+
const v = Math.trunc(n);
|
|
13
|
+
if (!Number.isFinite(v)) throw new InventoryError(`${label} must be a finite number`);
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
function requireQty(qty) {
|
|
17
|
+
const q = toCount(qty, "qty");
|
|
18
|
+
if (q < 0) throw new InventoryError("qty must be >= 0");
|
|
19
|
+
return q;
|
|
20
|
+
}
|
|
21
|
+
function createStock(onHand = 0) {
|
|
22
|
+
const n = toCount(onHand, "onHand");
|
|
23
|
+
if (n < 0) throw new InventoryError("onHand must be >= 0");
|
|
24
|
+
return { onHand: n, reserved: 0 };
|
|
25
|
+
}
|
|
26
|
+
function available(stock) {
|
|
27
|
+
return stock.onHand - stock.reserved;
|
|
28
|
+
}
|
|
29
|
+
function reserve(stock, qty) {
|
|
30
|
+
const q = requireQty(qty);
|
|
31
|
+
if (q > available(stock)) {
|
|
32
|
+
throw new InventoryError(
|
|
33
|
+
`Cannot reserve ${q}: only ${available(stock)} available`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return { onHand: stock.onHand, reserved: stock.reserved + q };
|
|
37
|
+
}
|
|
38
|
+
function release(stock, qty) {
|
|
39
|
+
const q = requireQty(qty);
|
|
40
|
+
return { onHand: stock.onHand, reserved: stock.reserved - Math.min(q, stock.reserved) };
|
|
41
|
+
}
|
|
42
|
+
function commit(stock, qty) {
|
|
43
|
+
const q = requireQty(qty);
|
|
44
|
+
if (q > stock.reserved) {
|
|
45
|
+
throw new InventoryError(
|
|
46
|
+
`Cannot commit ${q}: only ${stock.reserved} reserved`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return { onHand: stock.onHand - q, reserved: stock.reserved - q };
|
|
50
|
+
}
|
|
51
|
+
function restock(stock, qty) {
|
|
52
|
+
const q = requireQty(qty);
|
|
53
|
+
return { onHand: stock.onHand + q, reserved: stock.reserved };
|
|
54
|
+
}
|
|
55
|
+
function adjust(stock, delta) {
|
|
56
|
+
const d = toCount(delta, "delta");
|
|
57
|
+
return { onHand: Math.max(0, stock.onHand + d), reserved: stock.reserved };
|
|
58
|
+
}
|
|
59
|
+
function isLow(stock, threshold) {
|
|
60
|
+
return available(stock) <= toCount(threshold, "threshold");
|
|
61
|
+
}
|
|
62
|
+
function isOutOfStock(stock) {
|
|
63
|
+
return available(stock) <= 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
exports.InventoryError = InventoryError;
|
|
67
|
+
exports.adjust = adjust;
|
|
68
|
+
exports.available = available;
|
|
69
|
+
exports.commit = commit;
|
|
70
|
+
exports.createStock = createStock;
|
|
71
|
+
exports.isLow = isLow;
|
|
72
|
+
exports.isOutOfStock = isOutOfStock;
|
|
73
|
+
exports.release = release;
|
|
74
|
+
exports.reserve = reserve;
|
|
75
|
+
exports.restock = restock;
|
|
76
|
+
//# sourceMappingURL=index.cjs.map
|
|
77
|
+
//# 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/inventory โ a stock-tracking engine that prevents overselling.\n *\n * Bring-your-own-store: this package is a set of pure, immutable functions over\n * a plain `Stock` state object `{ onHand, reserved }`. You decide where the\n * state lives (a database row, a cache, a signal); this decides the maths.\n *\n * The core guarantee: you can never reserve or fulfil more than is available,\n * so a race between two checkouts fails loudly instead of overselling.\n */\n\n/** Stock state for a single SKU. Both fields are non-negative integers. */\nexport interface Stock {\n /** Physical units in the warehouse. */\n onHand: number;\n /** Units held for pending orders (not yet shipped). */\n reserved: number;\n}\n\n/** Thrown when an operation would oversell or is otherwise invalid. */\nexport class InventoryError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InventoryError\";\n // Restore the prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, InventoryError.prototype);\n }\n}\n\nfunction toCount(n: number, label: string): number {\n const v = Math.trunc(n);\n if (!Number.isFinite(v)) throw new InventoryError(`${label} must be a finite number`);\n return v;\n}\n\n/** Require a non-negative quantity for an operation. */\nfunction requireQty(qty: number): number {\n const q = toCount(qty, \"qty\");\n if (q < 0) throw new InventoryError(\"qty must be >= 0\");\n return q;\n}\n\n/** Create a new stock state with the given quantity on hand (default `0`). */\nexport function createStock(onHand = 0): Stock {\n const n = toCount(onHand, \"onHand\");\n if (n < 0) throw new InventoryError(\"onHand must be >= 0\");\n return { onHand: n, reserved: 0 };\n}\n\n/** Units that can still be reserved: `onHand - reserved`. */\nexport function available(stock: Stock): number {\n return stock.onHand - stock.reserved;\n}\n\n/**\n * Reserve `qty` units for a pending order. Increases `reserved`.\n *\n * @throws {InventoryError} if `qty` exceeds {@link available} (would oversell).\n */\nexport function reserve(stock: Stock, qty: number): Stock {\n const q = requireQty(qty);\n if (q > available(stock)) {\n throw new InventoryError(\n `Cannot reserve ${q}: only ${available(stock)} available`,\n );\n }\n return { onHand: stock.onHand, reserved: stock.reserved + q };\n}\n\n/**\n * Release a previously-held reservation (e.g. an abandoned cart). Never drops\n * `reserved` below `0` โ it releases at most what is currently reserved.\n */\nexport function release(stock: Stock, qty: number): Stock {\n const q = requireQty(qty);\n return { onHand: stock.onHand, reserved: stock.reserved - Math.min(q, stock.reserved) };\n}\n\n/**\n * Fulfil (ship) `qty` reserved units: decrements both `onHand` and `reserved`.\n *\n * @throws {InventoryError} if `qty` exceeds the currently reserved amount.\n */\nexport function commit(stock: Stock, qty: number): Stock {\n const q = requireQty(qty);\n if (q > stock.reserved) {\n throw new InventoryError(\n `Cannot commit ${q}: only ${stock.reserved} reserved`,\n );\n }\n return { onHand: stock.onHand - q, reserved: stock.reserved - q };\n}\n\n/** Add `qty` units to `onHand` (a delivery / restock). */\nexport function restock(stock: Stock, qty: number): Stock {\n const q = requireQty(qty);\n return { onHand: stock.onHand + q, reserved: stock.reserved };\n}\n\n/**\n * Apply a signed correction to `onHand` (stock-take, shrinkage, returns).\n * `onHand` is clamped at `0`; `reserved` is left untouched.\n */\nexport function adjust(stock: Stock, delta: number): Stock {\n const d = toCount(delta, \"delta\");\n return { onHand: Math.max(0, stock.onHand + d), reserved: stock.reserved };\n}\n\n/** `true` when {@link available} is at or below `threshold`. */\nexport function isLow(stock: Stock, threshold: number): boolean {\n return available(stock) <= toCount(threshold, \"threshold\");\n}\n\n/** `true` when nothing is available to reserve (`available <= 0`). */\nexport function isOutOfStock(stock: Stock): boolean {\n return available(stock) <= 0;\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/inventory โ a stock-tracking engine that prevents overselling.
|
|
3
|
+
*
|
|
4
|
+
* Bring-your-own-store: this package is a set of pure, immutable functions over
|
|
5
|
+
* a plain `Stock` state object `{ onHand, reserved }`. You decide where the
|
|
6
|
+
* state lives (a database row, a cache, a signal); this decides the maths.
|
|
7
|
+
*
|
|
8
|
+
* The core guarantee: you can never reserve or fulfil more than is available,
|
|
9
|
+
* so a race between two checkouts fails loudly instead of overselling.
|
|
10
|
+
*/
|
|
11
|
+
/** Stock state for a single SKU. Both fields are non-negative integers. */
|
|
12
|
+
interface Stock {
|
|
13
|
+
/** Physical units in the warehouse. */
|
|
14
|
+
onHand: number;
|
|
15
|
+
/** Units held for pending orders (not yet shipped). */
|
|
16
|
+
reserved: number;
|
|
17
|
+
}
|
|
18
|
+
/** Thrown when an operation would oversell or is otherwise invalid. */
|
|
19
|
+
declare class InventoryError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
/** Create a new stock state with the given quantity on hand (default `0`). */
|
|
23
|
+
declare function createStock(onHand?: number): Stock;
|
|
24
|
+
/** Units that can still be reserved: `onHand - reserved`. */
|
|
25
|
+
declare function available(stock: Stock): number;
|
|
26
|
+
/**
|
|
27
|
+
* Reserve `qty` units for a pending order. Increases `reserved`.
|
|
28
|
+
*
|
|
29
|
+
* @throws {InventoryError} if `qty` exceeds {@link available} (would oversell).
|
|
30
|
+
*/
|
|
31
|
+
declare function reserve(stock: Stock, qty: number): Stock;
|
|
32
|
+
/**
|
|
33
|
+
* Release a previously-held reservation (e.g. an abandoned cart). Never drops
|
|
34
|
+
* `reserved` below `0` โ it releases at most what is currently reserved.
|
|
35
|
+
*/
|
|
36
|
+
declare function release(stock: Stock, qty: number): Stock;
|
|
37
|
+
/**
|
|
38
|
+
* Fulfil (ship) `qty` reserved units: decrements both `onHand` and `reserved`.
|
|
39
|
+
*
|
|
40
|
+
* @throws {InventoryError} if `qty` exceeds the currently reserved amount.
|
|
41
|
+
*/
|
|
42
|
+
declare function commit(stock: Stock, qty: number): Stock;
|
|
43
|
+
/** Add `qty` units to `onHand` (a delivery / restock). */
|
|
44
|
+
declare function restock(stock: Stock, qty: number): Stock;
|
|
45
|
+
/**
|
|
46
|
+
* Apply a signed correction to `onHand` (stock-take, shrinkage, returns).
|
|
47
|
+
* `onHand` is clamped at `0`; `reserved` is left untouched.
|
|
48
|
+
*/
|
|
49
|
+
declare function adjust(stock: Stock, delta: number): Stock;
|
|
50
|
+
/** `true` when {@link available} is at or below `threshold`. */
|
|
51
|
+
declare function isLow(stock: Stock, threshold: number): boolean;
|
|
52
|
+
/** `true` when nothing is available to reserve (`available <= 0`). */
|
|
53
|
+
declare function isOutOfStock(stock: Stock): boolean;
|
|
54
|
+
|
|
55
|
+
export { InventoryError, type Stock, adjust, available, commit, createStock, isLow, isOutOfStock, release, reserve, restock };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/inventory โ a stock-tracking engine that prevents overselling.
|
|
3
|
+
*
|
|
4
|
+
* Bring-your-own-store: this package is a set of pure, immutable functions over
|
|
5
|
+
* a plain `Stock` state object `{ onHand, reserved }`. You decide where the
|
|
6
|
+
* state lives (a database row, a cache, a signal); this decides the maths.
|
|
7
|
+
*
|
|
8
|
+
* The core guarantee: you can never reserve or fulfil more than is available,
|
|
9
|
+
* so a race between two checkouts fails loudly instead of overselling.
|
|
10
|
+
*/
|
|
11
|
+
/** Stock state for a single SKU. Both fields are non-negative integers. */
|
|
12
|
+
interface Stock {
|
|
13
|
+
/** Physical units in the warehouse. */
|
|
14
|
+
onHand: number;
|
|
15
|
+
/** Units held for pending orders (not yet shipped). */
|
|
16
|
+
reserved: number;
|
|
17
|
+
}
|
|
18
|
+
/** Thrown when an operation would oversell or is otherwise invalid. */
|
|
19
|
+
declare class InventoryError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
/** Create a new stock state with the given quantity on hand (default `0`). */
|
|
23
|
+
declare function createStock(onHand?: number): Stock;
|
|
24
|
+
/** Units that can still be reserved: `onHand - reserved`. */
|
|
25
|
+
declare function available(stock: Stock): number;
|
|
26
|
+
/**
|
|
27
|
+
* Reserve `qty` units for a pending order. Increases `reserved`.
|
|
28
|
+
*
|
|
29
|
+
* @throws {InventoryError} if `qty` exceeds {@link available} (would oversell).
|
|
30
|
+
*/
|
|
31
|
+
declare function reserve(stock: Stock, qty: number): Stock;
|
|
32
|
+
/**
|
|
33
|
+
* Release a previously-held reservation (e.g. an abandoned cart). Never drops
|
|
34
|
+
* `reserved` below `0` โ it releases at most what is currently reserved.
|
|
35
|
+
*/
|
|
36
|
+
declare function release(stock: Stock, qty: number): Stock;
|
|
37
|
+
/**
|
|
38
|
+
* Fulfil (ship) `qty` reserved units: decrements both `onHand` and `reserved`.
|
|
39
|
+
*
|
|
40
|
+
* @throws {InventoryError} if `qty` exceeds the currently reserved amount.
|
|
41
|
+
*/
|
|
42
|
+
declare function commit(stock: Stock, qty: number): Stock;
|
|
43
|
+
/** Add `qty` units to `onHand` (a delivery / restock). */
|
|
44
|
+
declare function restock(stock: Stock, qty: number): Stock;
|
|
45
|
+
/**
|
|
46
|
+
* Apply a signed correction to `onHand` (stock-take, shrinkage, returns).
|
|
47
|
+
* `onHand` is clamped at `0`; `reserved` is left untouched.
|
|
48
|
+
*/
|
|
49
|
+
declare function adjust(stock: Stock, delta: number): Stock;
|
|
50
|
+
/** `true` when {@link available} is at or below `threshold`. */
|
|
51
|
+
declare function isLow(stock: Stock, threshold: number): boolean;
|
|
52
|
+
/** `true` when nothing is available to reserve (`available <= 0`). */
|
|
53
|
+
declare function isOutOfStock(stock: Stock): boolean;
|
|
54
|
+
|
|
55
|
+
export { InventoryError, type Stock, adjust, available, commit, createStock, isLow, isOutOfStock, release, reserve, restock };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var InventoryError = class _InventoryError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "InventoryError";
|
|
6
|
+
Object.setPrototypeOf(this, _InventoryError.prototype);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function toCount(n, label) {
|
|
10
|
+
const v = Math.trunc(n);
|
|
11
|
+
if (!Number.isFinite(v)) throw new InventoryError(`${label} must be a finite number`);
|
|
12
|
+
return v;
|
|
13
|
+
}
|
|
14
|
+
function requireQty(qty) {
|
|
15
|
+
const q = toCount(qty, "qty");
|
|
16
|
+
if (q < 0) throw new InventoryError("qty must be >= 0");
|
|
17
|
+
return q;
|
|
18
|
+
}
|
|
19
|
+
function createStock(onHand = 0) {
|
|
20
|
+
const n = toCount(onHand, "onHand");
|
|
21
|
+
if (n < 0) throw new InventoryError("onHand must be >= 0");
|
|
22
|
+
return { onHand: n, reserved: 0 };
|
|
23
|
+
}
|
|
24
|
+
function available(stock) {
|
|
25
|
+
return stock.onHand - stock.reserved;
|
|
26
|
+
}
|
|
27
|
+
function reserve(stock, qty) {
|
|
28
|
+
const q = requireQty(qty);
|
|
29
|
+
if (q > available(stock)) {
|
|
30
|
+
throw new InventoryError(
|
|
31
|
+
`Cannot reserve ${q}: only ${available(stock)} available`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return { onHand: stock.onHand, reserved: stock.reserved + q };
|
|
35
|
+
}
|
|
36
|
+
function release(stock, qty) {
|
|
37
|
+
const q = requireQty(qty);
|
|
38
|
+
return { onHand: stock.onHand, reserved: stock.reserved - Math.min(q, stock.reserved) };
|
|
39
|
+
}
|
|
40
|
+
function commit(stock, qty) {
|
|
41
|
+
const q = requireQty(qty);
|
|
42
|
+
if (q > stock.reserved) {
|
|
43
|
+
throw new InventoryError(
|
|
44
|
+
`Cannot commit ${q}: only ${stock.reserved} reserved`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return { onHand: stock.onHand - q, reserved: stock.reserved - q };
|
|
48
|
+
}
|
|
49
|
+
function restock(stock, qty) {
|
|
50
|
+
const q = requireQty(qty);
|
|
51
|
+
return { onHand: stock.onHand + q, reserved: stock.reserved };
|
|
52
|
+
}
|
|
53
|
+
function adjust(stock, delta) {
|
|
54
|
+
const d = toCount(delta, "delta");
|
|
55
|
+
return { onHand: Math.max(0, stock.onHand + d), reserved: stock.reserved };
|
|
56
|
+
}
|
|
57
|
+
function isLow(stock, threshold) {
|
|
58
|
+
return available(stock) <= toCount(threshold, "threshold");
|
|
59
|
+
}
|
|
60
|
+
function isOutOfStock(stock) {
|
|
61
|
+
return available(stock) <= 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { InventoryError, adjust, available, commit, createStock, isLow, isOutOfStock, release, reserve, restock };
|
|
65
|
+
//# sourceMappingURL=index.js.map
|
|
66
|
+
//# 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/inventory โ a stock-tracking engine that prevents overselling.\n *\n * Bring-your-own-store: this package is a set of pure, immutable functions over\n * a plain `Stock` state object `{ onHand, reserved }`. You decide where the\n * state lives (a database row, a cache, a signal); this decides the maths.\n *\n * The core guarantee: you can never reserve or fulfil more than is available,\n * so a race between two checkouts fails loudly instead of overselling.\n */\n\n/** Stock state for a single SKU. Both fields are non-negative integers. */\nexport interface Stock {\n /** Physical units in the warehouse. */\n onHand: number;\n /** Units held for pending orders (not yet shipped). */\n reserved: number;\n}\n\n/** Thrown when an operation would oversell or is otherwise invalid. */\nexport class InventoryError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InventoryError\";\n // Restore the prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, InventoryError.prototype);\n }\n}\n\nfunction toCount(n: number, label: string): number {\n const v = Math.trunc(n);\n if (!Number.isFinite(v)) throw new InventoryError(`${label} must be a finite number`);\n return v;\n}\n\n/** Require a non-negative quantity for an operation. */\nfunction requireQty(qty: number): number {\n const q = toCount(qty, \"qty\");\n if (q < 0) throw new InventoryError(\"qty must be >= 0\");\n return q;\n}\n\n/** Create a new stock state with the given quantity on hand (default `0`). */\nexport function createStock(onHand = 0): Stock {\n const n = toCount(onHand, \"onHand\");\n if (n < 0) throw new InventoryError(\"onHand must be >= 0\");\n return { onHand: n, reserved: 0 };\n}\n\n/** Units that can still be reserved: `onHand - reserved`. */\nexport function available(stock: Stock): number {\n return stock.onHand - stock.reserved;\n}\n\n/**\n * Reserve `qty` units for a pending order. Increases `reserved`.\n *\n * @throws {InventoryError} if `qty` exceeds {@link available} (would oversell).\n */\nexport function reserve(stock: Stock, qty: number): Stock {\n const q = requireQty(qty);\n if (q > available(stock)) {\n throw new InventoryError(\n `Cannot reserve ${q}: only ${available(stock)} available`,\n );\n }\n return { onHand: stock.onHand, reserved: stock.reserved + q };\n}\n\n/**\n * Release a previously-held reservation (e.g. an abandoned cart). Never drops\n * `reserved` below `0` โ it releases at most what is currently reserved.\n */\nexport function release(stock: Stock, qty: number): Stock {\n const q = requireQty(qty);\n return { onHand: stock.onHand, reserved: stock.reserved - Math.min(q, stock.reserved) };\n}\n\n/**\n * Fulfil (ship) `qty` reserved units: decrements both `onHand` and `reserved`.\n *\n * @throws {InventoryError} if `qty` exceeds the currently reserved amount.\n */\nexport function commit(stock: Stock, qty: number): Stock {\n const q = requireQty(qty);\n if (q > stock.reserved) {\n throw new InventoryError(\n `Cannot commit ${q}: only ${stock.reserved} reserved`,\n );\n }\n return { onHand: stock.onHand - q, reserved: stock.reserved - q };\n}\n\n/** Add `qty` units to `onHand` (a delivery / restock). */\nexport function restock(stock: Stock, qty: number): Stock {\n const q = requireQty(qty);\n return { onHand: stock.onHand + q, reserved: stock.reserved };\n}\n\n/**\n * Apply a signed correction to `onHand` (stock-take, shrinkage, returns).\n * `onHand` is clamped at `0`; `reserved` is left untouched.\n */\nexport function adjust(stock: Stock, delta: number): Stock {\n const d = toCount(delta, \"delta\");\n return { onHand: Math.max(0, stock.onHand + d), reserved: stock.reserved };\n}\n\n/** `true` when {@link available} is at or below `threshold`. */\nexport function isLow(stock: Stock, threshold: number): boolean {\n return available(stock) <= toCount(threshold, \"threshold\");\n}\n\n/** `true` when nothing is available to reserve (`available <= 0`). */\nexport function isOutOfStock(stock: Stock): boolean {\n return available(stock) <= 0;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/inventory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Stock-tracking engine that prevents overselling โ reserve, release, commit & restock over a plain { onHand, reserved } state. Immutable, bring-your-own-store, throws before it oversells. 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
|
+
"inventory",
|
|
31
|
+
"stock",
|
|
32
|
+
"stock-management",
|
|
33
|
+
"reservation",
|
|
34
|
+
"oversell",
|
|
35
|
+
"ecommerce",
|
|
36
|
+
"warehouse",
|
|
37
|
+
"fulfilment",
|
|
38
|
+
"immutable",
|
|
39
|
+
"headless",
|
|
40
|
+
"isomorphic",
|
|
41
|
+
"typescript"
|
|
42
|
+
],
|
|
43
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
44
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
45
|
+
"homepage": "https://developer.lacspace.com/packages/inventory",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/lacspace/npm-packages.git",
|
|
49
|
+
"directory": "inventory"
|
|
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
|
+
}
|