@porulle/plugin-warehouse 0.1.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/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @porulle/plugin-warehouse
2
+
3
+ Inter-warehouse stock transfers with approval and dispatch/receive, wastage notes, and stock reconciliations.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @porulle/plugin-warehouse
9
+ ```
10
+
11
+ Add to `commerce.config.ts`:
12
+
13
+ ```typescript
14
+ import { warehousePlugin } from "@porulle/plugin-warehouse";
15
+
16
+ export default defineConfig({
17
+ plugins: [warehousePlugin()],
18
+ });
19
+ ```
20
+
21
+ Add to `drizzle.config.ts`:
22
+
23
+ ```typescript
24
+ schema: [
25
+ "./node_modules/@porulle/plugin-warehouse/src/schema.ts",
26
+ // ...
27
+ ],
28
+ ```
29
+
30
+ ## What it does
31
+
32
+ Models bins (schema), transfer headers/lines with workflow states, wastage with approval, and cycle-count style reconciliations with submit/approve steps.
33
+
34
+ ## Routes exposed
35
+
36
+ **`/warehouse`** — transfers: `POST/GET /transfers`, `GET /transfers/{id}`, `POST .../approve|dispatch|receive`; wastage: `POST/GET /wastage`, `POST /wastage/{id}/approve`; reconciliations: `POST/GET /reconciliations`, `GET /reconciliations/{id}`, `POST .../submit|approve`
37
+
38
+ ## Hooks
39
+
40
+ **Emitted:** none.
41
+
42
+ **Consumed:** none.
43
+
44
+ ## MCP tools
45
+
46
+ **`warehouse`** — `list_transfers`, `get_transfer`, `approve_transfer`, `dispatch_transfer`, `list_wastage`, `approve_wastage`, `list_reconciliations`, `get_reconciliation`, `submit_reconciliation`, `approve_reconciliation`
47
+
48
+ ## Configuration options
49
+
50
+ None (`warehousePlugin()` takes no options).
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,6 @@
1
+ export type { Db } from "./types.js";
2
+ export { TransferService } from "./services/transfer-service.js";
3
+ export { WastageService } from "./services/wastage-service.js";
4
+ export { ReconciliationService } from "./services/reconciliation-service.js";
5
+ export declare function warehousePlugin(): import("@porulle/core").CommercePlugin;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,YAAY,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAE7E,wBAAgB,eAAe,2CAiB9B"}
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ import { defineCommercePlugin } from "@porulle/core";
2
+ import { warehouseBins, stockTransfers, stockTransferItems, wastageNotes, wastageNoteItems, stockReconciliations, reconciliationItems } from "./schema.js";
3
+ import { TransferService } from "./services/transfer-service.js";
4
+ import { WastageService } from "./services/wastage-service.js";
5
+ import { ReconciliationService } from "./services/reconciliation-service.js";
6
+ import { buildWarehouseRoutes } from "./routes/warehouse.js";
7
+ export { TransferService } from "./services/transfer-service.js";
8
+ export { WastageService } from "./services/wastage-service.js";
9
+ export { ReconciliationService } from "./services/reconciliation-service.js";
10
+ export function warehousePlugin() {
11
+ return defineCommercePlugin({
12
+ id: "warehouse",
13
+ version: "1.0.0",
14
+ permissions: [
15
+ { scope: "warehouse:admin", description: "Approve transfers, wastage, reconciliations." },
16
+ { scope: "warehouse:operate", description: "Create transfers, wastage notes, reconciliations." },
17
+ { scope: "warehouse:read", description: "View transfers, wastage, reconciliations." },
18
+ ],
19
+ schema: () => ({ warehouseBins, stockTransfers, stockTransferItems, wastageNotes, wastageNoteItems, stockReconciliations, reconciliationItems }),
20
+ hooks: () => [],
21
+ routes: (ctx) => {
22
+ const db = ctx.database.db;
23
+ if (!db)
24
+ return [];
25
+ return buildWarehouseRoutes(new TransferService(db), new WastageService(db), new ReconciliationService(db), ctx);
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,11 @@
1
+ import type { TransferService } from "../services/transfer-service.js";
2
+ import type { WastageService } from "../services/wastage-service.js";
3
+ import type { ReconciliationService } from "../services/reconciliation-service.js";
4
+ import type { PluginRouteRegistration } from "@porulle/core";
5
+ export declare function buildWarehouseRoutes(transferSvc: TransferService, wastageSvc: WastageService, recSvc: ReconciliationService, ctx: {
6
+ services?: Record<string, unknown>;
7
+ database?: {
8
+ db: unknown;
9
+ };
10
+ }): PluginRouteRegistration[];
11
+ //# sourceMappingURL=warehouse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"warehouse.d.ts","sourceRoot":"","sources":["../../src/routes/warehouse.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,EAAE,qBAAqB,EACvF,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACtE,uBAAuB,EAAE,CA2H3B"}
@@ -0,0 +1,125 @@
1
+ import { router } from "@porulle/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ export function buildWarehouseRoutes(transferSvc, wastageSvc, recSvc, ctx) {
4
+ const r = router("Warehouse", "/warehouse", ctx);
5
+ // Transfers
6
+ r.post("/transfers").summary("Create stock transfer").permission("warehouse:operate")
7
+ .input(z.object({
8
+ fromWarehouseId: z.string().uuid(), toWarehouseId: z.string().uuid(), type: z.enum(["requisition", "direct", "return"]).optional(), notes: z.string().optional(),
9
+ items: z.array(z.object({ entityId: z.string().uuid(), variantId: z.string().uuid().optional(), itemName: z.string(), quantityRequested: z.number().int().positive(), batchNumber: z.string().optional() })).min(1),
10
+ }))
11
+ .handler(async ({ input, actor, orgId }) => {
12
+ const body = input;
13
+ const result = await transferSvc.create(orgId, { ...body, requestedBy: actor.userId });
14
+ if (!result.ok)
15
+ throw new Error(result.error);
16
+ return result.value;
17
+ });
18
+ r.get("/transfers").summary("List transfers").permission("warehouse:read")
19
+ .query(z.object({ status: z.string().optional() }))
20
+ .handler(async ({ query, orgId }) => {
21
+ const result = await transferSvc.list(orgId, query.status);
22
+ if (!result.ok)
23
+ throw new Error(result.error);
24
+ return result.value;
25
+ });
26
+ r.get("/transfers/{id}").summary("Get transfer with items").permission("warehouse:read")
27
+ .handler(async ({ params, orgId }) => {
28
+ const result = await transferSvc.getById(orgId, params.id);
29
+ if (!result.ok)
30
+ throw new Error(result.error);
31
+ return result.value;
32
+ });
33
+ r.post("/transfers/{id}/approve").summary("Approve transfer").permission("warehouse:admin")
34
+ .handler(async ({ params, actor, orgId }) => {
35
+ const result = await transferSvc.approve(orgId, params.id, actor.userId);
36
+ if (!result.ok)
37
+ throw new Error(result.error);
38
+ return result.value;
39
+ });
40
+ r.post("/transfers/{id}/dispatch").summary("Dispatch transfer").permission("warehouse:operate")
41
+ .handler(async ({ params, orgId }) => {
42
+ const result = await transferSvc.dispatch(orgId, params.id);
43
+ if (!result.ok)
44
+ throw new Error(result.error);
45
+ return result.value;
46
+ });
47
+ r.post("/transfers/{id}/receive").summary("Receive transfer").permission("warehouse:operate")
48
+ .input(z.object({ items: z.array(z.object({ itemId: z.string().uuid(), quantityReceived: z.number().int() })).min(1) }))
49
+ .handler(async ({ params, input, orgId }) => {
50
+ const body = input;
51
+ const result = await transferSvc.receive(orgId, params.id, body.items);
52
+ if (!result.ok)
53
+ throw new Error(result.error);
54
+ return result.value;
55
+ });
56
+ // Wastage
57
+ r.post("/wastage").summary("Create wastage note").permission("warehouse:operate")
58
+ .input(z.object({
59
+ warehouseId: z.string().uuid(), type: z.enum(["spoilage", "damage", "expiry", "theft", "prep_waste", "other"]), notes: z.string().optional(),
60
+ items: z.array(z.object({ entityId: z.string().uuid(), variantId: z.string().uuid().optional(), itemName: z.string(), quantity: z.number().int().positive(), unitCost: z.number().int(), reason: z.string().optional(), batchNumber: z.string().optional() })).min(1),
61
+ }))
62
+ .handler(async ({ input, actor, orgId }) => {
63
+ const body = input;
64
+ const result = await wastageSvc.create(orgId, { ...body, recordedBy: actor.userId });
65
+ if (!result.ok)
66
+ throw new Error(result.error);
67
+ return result.value;
68
+ });
69
+ r.get("/wastage").summary("List wastage notes").permission("warehouse:read")
70
+ .handler(async ({ orgId }) => {
71
+ const result = await wastageSvc.list(orgId);
72
+ if (!result.ok)
73
+ throw new Error(result.error);
74
+ return result.value;
75
+ });
76
+ r.post("/wastage/{id}/approve").summary("Approve wastage").permission("warehouse:admin")
77
+ .handler(async ({ params, actor, orgId }) => {
78
+ const result = await wastageSvc.approve(orgId, params.id, actor.userId);
79
+ if (!result.ok)
80
+ throw new Error(result.error);
81
+ return result.value;
82
+ });
83
+ // Reconciliation
84
+ r.post("/reconciliations").summary("Create stock reconciliation").permission("warehouse:operate")
85
+ .input(z.object({
86
+ warehouseId: z.string().uuid(),
87
+ items: z.array(z.object({ entityId: z.string().uuid(), variantId: z.string().uuid().optional(), itemName: z.string(), systemQuantity: z.number().int(), physicalQuantity: z.number().int(), notes: z.string().optional() })).min(1),
88
+ }))
89
+ .handler(async ({ input, actor, orgId }) => {
90
+ const body = input;
91
+ const result = await recSvc.create(orgId, { ...body, countedBy: actor.userId });
92
+ if (!result.ok)
93
+ throw new Error(result.error);
94
+ return result.value;
95
+ });
96
+ r.get("/reconciliations").summary("List reconciliations").permission("warehouse:read")
97
+ .handler(async ({ orgId }) => {
98
+ const result = await recSvc.list(orgId);
99
+ if (!result.ok)
100
+ throw new Error(result.error);
101
+ return result.value;
102
+ });
103
+ r.get("/reconciliations/{id}").summary("Get reconciliation with items").permission("warehouse:read")
104
+ .handler(async ({ params, orgId }) => {
105
+ const result = await recSvc.getById(orgId, params.id);
106
+ if (!result.ok)
107
+ throw new Error(result.error);
108
+ return result.value;
109
+ });
110
+ r.post("/reconciliations/{id}/submit").summary("Submit reconciliation").permission("warehouse:operate")
111
+ .handler(async ({ params, orgId }) => {
112
+ const result = await recSvc.submit(orgId, params.id);
113
+ if (!result.ok)
114
+ throw new Error(result.error);
115
+ return result.value;
116
+ });
117
+ r.post("/reconciliations/{id}/approve").summary("Approve reconciliation").permission("warehouse:admin")
118
+ .handler(async ({ params, actor, orgId }) => {
119
+ const result = await recSvc.approve(orgId, params.id, actor.userId);
120
+ if (!result.ok)
121
+ throw new Error(result.error);
122
+ return result.value;
123
+ });
124
+ return r.routes();
125
+ }