@porulle/plugin-procurement 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-procurement
2
+
3
+ Suppliers with catalog links, purchase orders through approval, and goods-received notes for inbound receiving.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @porulle/plugin-procurement
9
+ ```
10
+
11
+ Add to `commerce.config.ts`:
12
+
13
+ ```typescript
14
+ import { procurementPlugin } from "@porulle/plugin-procurement";
15
+
16
+ export default defineConfig({
17
+ plugins: [procurementPlugin()],
18
+ });
19
+ ```
20
+
21
+ Add to `drizzle.config.ts`:
22
+
23
+ ```typescript
24
+ schema: [
25
+ "./node_modules/@porulle/plugin-procurement/src/schema.ts",
26
+ // ...
27
+ ],
28
+ ```
29
+
30
+ ## What it does
31
+
32
+ Manages supplier master data, links sellable items to suppliers, creates and approves POs, records GRNs, and accepts GRNs into inventory context via services (see procurement routes for permissions).
33
+
34
+ ## Routes exposed
35
+
36
+ **`/procurement`** — `POST/GET /suppliers`, `GET /suppliers/{id}`, `POST /suppliers/{id}/items`, `POST/GET /purchase-orders`, `GET /purchase-orders/{id}`, `POST .../submit|approve|cancel`, `POST/GET /grn`, `GET /grn/{id}`, `POST /grn/{id}/accept`
37
+
38
+ ## Hooks
39
+
40
+ **Emitted:** none.
41
+
42
+ **Consumed:** none.
43
+
44
+ ## MCP tools
45
+
46
+ **`procurement`** — `list_suppliers`, `get_supplier`, `list_po`, `get_po`, `approve_po`, `cancel_po`, `list_grn`, `get_grn`, `accept_grn`
47
+
48
+ ## Configuration options
49
+
50
+ None (`procurementPlugin()` takes no options).
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,6 @@
1
+ export type { Db } from "./types.js";
2
+ export { SupplierService } from "./services/supplier-service.js";
3
+ export { PurchaseOrderService } from "./services/po-service.js";
4
+ export { GRNService } from "./services/grn-service.js";
5
+ export declare function procurementPlugin(): 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,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAEvD,wBAAgB,iBAAiB,2CAiBhC"}
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ import { defineCommercePlugin } from "@porulle/core";
2
+ import { suppliers, supplierItems, purchaseOrders, purchaseOrderItems, goodsReceivedNotes, grnItems } from "./schema.js";
3
+ import { SupplierService } from "./services/supplier-service.js";
4
+ import { PurchaseOrderService } from "./services/po-service.js";
5
+ import { GRNService } from "./services/grn-service.js";
6
+ import { buildProcurementRoutes } from "./routes/procurement.js";
7
+ export { SupplierService } from "./services/supplier-service.js";
8
+ export { PurchaseOrderService } from "./services/po-service.js";
9
+ export { GRNService } from "./services/grn-service.js";
10
+ export function procurementPlugin() {
11
+ return defineCommercePlugin({
12
+ id: "procurement",
13
+ version: "1.0.0",
14
+ permissions: [
15
+ { scope: "procurement:admin", description: "Approve POs, manage suppliers, accept GRNs." },
16
+ { scope: "procurement:create", description: "Create POs, create GRNs." },
17
+ { scope: "procurement:read", description: "View POs, GRNs, suppliers." },
18
+ ],
19
+ schema: () => ({ suppliers, supplierItems, purchaseOrders, purchaseOrderItems, goodsReceivedNotes, grnItems }),
20
+ hooks: () => [],
21
+ routes: (ctx) => {
22
+ const db = ctx.database.db;
23
+ if (!db)
24
+ return [];
25
+ return buildProcurementRoutes(new SupplierService(db), new PurchaseOrderService(db), new GRNService(db), ctx);
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,11 @@
1
+ import type { SupplierService } from "../services/supplier-service.js";
2
+ import type { PurchaseOrderService } from "../services/po-service.js";
3
+ import type { GRNService } from "../services/grn-service.js";
4
+ import type { PluginRouteRegistration } from "@porulle/core";
5
+ export declare function buildProcurementRoutes(supplierSvc: SupplierService, poSvc: PurchaseOrderService, grnSvc: GRNService, ctx: {
6
+ services?: Record<string, unknown>;
7
+ database?: {
8
+ db: unknown;
9
+ };
10
+ }): PluginRouteRegistration[];
11
+ //# sourceMappingURL=procurement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procurement.d.ts","sourceRoot":"","sources":["../../src/routes/procurement.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,eAAe,EAAE,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,UAAU,EAC7E,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,CA4H3B"}
@@ -0,0 +1,126 @@
1
+ import { router } from "@porulle/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ export function buildProcurementRoutes(supplierSvc, poSvc, grnSvc, ctx) {
4
+ const r = router("Procurement", "/procurement", ctx);
5
+ // Suppliers
6
+ r.post("/suppliers").summary("Register supplier").permission("procurement:admin")
7
+ .input(z.object({ name: z.string().min(1), code: z.string().min(1), contactName: z.string().optional(), contactEmail: z.string().email().optional(), contactPhone: z.string().optional(), paymentTermsDays: z.number().int().optional(), currency: z.string().optional() }))
8
+ .handler(async ({ input, orgId }) => {
9
+ const result = await supplierSvc.create(orgId, input);
10
+ if (!result.ok)
11
+ throw new Error(result.error);
12
+ return result.value;
13
+ });
14
+ r.get("/suppliers").summary("List suppliers").permission("procurement:read")
15
+ .handler(async ({ orgId }) => {
16
+ const result = await supplierSvc.list(orgId);
17
+ if (!result.ok)
18
+ throw new Error(result.error);
19
+ return result.value;
20
+ });
21
+ r.get("/suppliers/{id}").summary("Get supplier with items").permission("procurement:read")
22
+ .handler(async ({ params, orgId }) => {
23
+ const result = await supplierSvc.getById(orgId, params.id);
24
+ if (!result.ok)
25
+ throw new Error(result.error);
26
+ return result.value;
27
+ });
28
+ r.post("/suppliers/{id}/items").summary("Link item to supplier").permission("procurement:admin")
29
+ .input(z.object({ entityId: z.string().uuid(), variantId: z.string().uuid().optional(), supplierSku: z.string().optional(), unitCost: z.number().int().positive(), minOrderQuantity: z.number().int().optional(), leadTimeDays: z.number().int().optional(), isPreferred: z.boolean().optional() }))
30
+ .handler(async ({ params, input }) => {
31
+ const result = await supplierSvc.addItem(params.id, input);
32
+ if (!result.ok)
33
+ throw new Error(result.error);
34
+ return result.value;
35
+ });
36
+ // Purchase Orders
37
+ r.post("/purchase-orders").summary("Create purchase order").permission("procurement:create")
38
+ .input(z.object({
39
+ supplierId: z.string().uuid(), warehouseId: z.string().uuid(), notes: z.string().optional(),
40
+ expectedDelivery: z.string().optional(),
41
+ items: z.array(z.object({ entityId: z.string().uuid(), variantId: z.string().uuid().optional(), itemName: z.string(), quantityOrdered: z.number().int().positive(), unitCost: z.number().int().positive() })).min(1),
42
+ }))
43
+ .handler(async ({ input, actor, orgId }) => {
44
+ const body = input;
45
+ const result = await poSvc.create(orgId, { ...body, requestedBy: actor.userId });
46
+ if (!result.ok)
47
+ throw new Error(result.error);
48
+ return result.value;
49
+ });
50
+ r.get("/purchase-orders").summary("List purchase orders").permission("procurement:read")
51
+ .query(z.object({ status: z.string().optional() }))
52
+ .handler(async ({ query, orgId }) => {
53
+ const result = await poSvc.list(orgId, query.status);
54
+ if (!result.ok)
55
+ throw new Error(result.error);
56
+ return result.value;
57
+ });
58
+ r.get("/purchase-orders/{id}").summary("Get PO with items").permission("procurement:read")
59
+ .handler(async ({ params, orgId }) => {
60
+ const result = await poSvc.getById(orgId, params.id);
61
+ if (!result.ok)
62
+ throw new Error(result.error);
63
+ return result.value;
64
+ });
65
+ r.post("/purchase-orders/{id}/submit").summary("Submit PO for approval").permission("procurement:create")
66
+ .handler(async ({ params, orgId }) => {
67
+ const result = await poSvc.submit(orgId, params.id);
68
+ if (!result.ok)
69
+ throw new Error(result.error);
70
+ return result.value;
71
+ });
72
+ r.post("/purchase-orders/{id}/approve").summary("Approve PO").permission("procurement:admin")
73
+ .handler(async ({ params, actor, orgId }) => {
74
+ const result = await poSvc.approve(orgId, params.id, actor.userId);
75
+ if (!result.ok)
76
+ throw new Error(result.error);
77
+ return result.value;
78
+ });
79
+ r.post("/purchase-orders/{id}/cancel").summary("Cancel PO").permission("procurement:admin")
80
+ .handler(async ({ params, orgId }) => {
81
+ const result = await poSvc.cancel(orgId, params.id);
82
+ if (!result.ok)
83
+ throw new Error(result.error);
84
+ return result.value;
85
+ });
86
+ // GRN
87
+ r.post("/grn").summary("Create GRN").permission("procurement:create")
88
+ .input(z.object({
89
+ poId: z.string().uuid(), supplierId: z.string().uuid(), warehouseId: z.string().uuid(), notes: z.string().optional(),
90
+ items: z.array(z.object({
91
+ poItemId: z.string().uuid(), entityId: z.string().uuid(), variantId: z.string().uuid().optional(),
92
+ quantityOrdered: z.number().int(), quantityReceived: z.number().int(), quantityAccepted: z.number().int(),
93
+ quantityRejected: z.number().int().optional(), rejectionReason: z.string().optional(),
94
+ batchNumber: z.string().optional(), expiryDate: z.string().optional(), unitCost: z.number().int(),
95
+ })).min(1),
96
+ }))
97
+ .handler(async ({ input, actor, orgId }) => {
98
+ const body = input;
99
+ const result = await grnSvc.create(orgId, { ...body, receivedBy: actor.userId });
100
+ if (!result.ok)
101
+ throw new Error(result.error);
102
+ return result.value;
103
+ });
104
+ r.get("/grn").summary("List GRNs").permission("procurement:read")
105
+ .handler(async ({ orgId }) => {
106
+ const result = await grnSvc.list(orgId);
107
+ if (!result.ok)
108
+ throw new Error(result.error);
109
+ return result.value;
110
+ });
111
+ r.get("/grn/{id}").summary("Get GRN with items").permission("procurement:read")
112
+ .handler(async ({ params, orgId }) => {
113
+ const result = await grnSvc.getById(orgId, params.id);
114
+ if (!result.ok)
115
+ throw new Error(result.error);
116
+ return result.value;
117
+ });
118
+ r.post("/grn/{id}/accept").summary("Accept GRN").permission("procurement:admin")
119
+ .handler(async ({ params, orgId }) => {
120
+ const result = await grnSvc.accept(orgId, params.id);
121
+ if (!result.ok)
122
+ throw new Error(result.error);
123
+ return result.value;
124
+ });
125
+ return r.routes();
126
+ }