@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 +54 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/routes/procurement.d.ts +11 -0
- package/dist/routes/procurement.d.ts.map +1 -0
- package/dist/routes/procurement.js +126 -0
- package/dist/schema.d.ts +1392 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +109 -0
- package/dist/services/grn-service.d.ts +34 -0
- package/dist/services/grn-service.d.ts.map +1 -0
- package/dist/services/grn-service.js +74 -0
- package/dist/services/po-service.d.ts +31 -0
- package/dist/services/po-service.d.ts.map +1 -0
- package/dist/services/po-service.js +77 -0
- package/dist/services/supplier-service.d.ts +30 -0
- package/dist/services/supplier-service.d.ts.map +1 -0
- package/dist/services/supplier-service.js +32 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +58 -0
- package/src/index.ts +29 -0
- package/src/routes/procurement.ts +135 -0
- package/src/schema.ts +115 -0
- package/src/services/grn-service.ts +91 -0
- package/src/services/po-service.ts +85 -0
- package/src/services/supplier-service.ts +40 -0
- package/src/types.ts +9 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkBnB,CAAC;AAEJ,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAcvB,CAAC;AAEJ,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBxB,CAAC;AAEJ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAc5B,CAAC;AAEJ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiB5B,CAAC;AAEJ,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiBlB,CAAC"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { pgTable, uuid, text, integer, boolean, timestamp, jsonb, index, uniqueIndex } from "@porulle/core/drizzle";
|
|
2
|
+
export const suppliers = pgTable("suppliers", {
|
|
3
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
4
|
+
organizationId: text("organization_id").notNull(),
|
|
5
|
+
name: text("name").notNull(),
|
|
6
|
+
code: text("code").notNull(),
|
|
7
|
+
contactName: text("contact_name"),
|
|
8
|
+
contactEmail: text("contact_email"),
|
|
9
|
+
contactPhone: text("contact_phone"),
|
|
10
|
+
paymentTermsDays: integer("payment_terms_days").notNull().default(30),
|
|
11
|
+
currency: text("currency").notNull().default("USD"),
|
|
12
|
+
taxId: text("tax_id"),
|
|
13
|
+
isActive: boolean("is_active").notNull().default(true),
|
|
14
|
+
metadata: jsonb("metadata").$type().default({}),
|
|
15
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
16
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
17
|
+
}, (table) => ({
|
|
18
|
+
orgIdx: index("idx_suppliers_org").on(table.organizationId),
|
|
19
|
+
codeUnique: uniqueIndex("suppliers_org_code_unique").on(table.organizationId, table.code),
|
|
20
|
+
}));
|
|
21
|
+
export const supplierItems = pgTable("supplier_items", {
|
|
22
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
23
|
+
supplierId: uuid("supplier_id").references(() => suppliers.id, { onDelete: "cascade" }).notNull(),
|
|
24
|
+
entityId: uuid("entity_id").notNull(),
|
|
25
|
+
variantId: uuid("variant_id"),
|
|
26
|
+
supplierSku: text("supplier_sku"),
|
|
27
|
+
unitCost: integer("unit_cost").notNull(),
|
|
28
|
+
minOrderQuantity: integer("min_order_quantity").notNull().default(1),
|
|
29
|
+
leadTimeDays: integer("lead_time_days").notNull().default(1),
|
|
30
|
+
isPreferred: boolean("is_preferred").notNull().default(false),
|
|
31
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
32
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
33
|
+
}, (table) => ({
|
|
34
|
+
supplierIdx: index("idx_supplier_items_supplier").on(table.supplierId),
|
|
35
|
+
}));
|
|
36
|
+
export const purchaseOrders = pgTable("purchase_orders", {
|
|
37
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
38
|
+
organizationId: text("organization_id").notNull(),
|
|
39
|
+
poNumber: text("po_number").notNull(),
|
|
40
|
+
supplierId: uuid("supplier_id").references(() => suppliers.id).notNull(),
|
|
41
|
+
status: text("status", { enum: ["draft", "pending_approval", "approved", "sent", "partially_received", "received", "cancelled"] }).notNull().default("draft"),
|
|
42
|
+
warehouseId: uuid("warehouse_id").notNull(),
|
|
43
|
+
requestedBy: text("requested_by").notNull(),
|
|
44
|
+
approvedBy: text("approved_by"),
|
|
45
|
+
approvedAt: timestamp("approved_at", { withTimezone: true }),
|
|
46
|
+
expectedDelivery: timestamp("expected_delivery", { withTimezone: true }),
|
|
47
|
+
subtotal: integer("subtotal").notNull().default(0),
|
|
48
|
+
taxTotal: integer("tax_total").notNull().default(0),
|
|
49
|
+
grandTotal: integer("grand_total").notNull().default(0),
|
|
50
|
+
notes: text("notes"),
|
|
51
|
+
metadata: jsonb("metadata").$type().default({}),
|
|
52
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
53
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
54
|
+
}, (table) => ({
|
|
55
|
+
orgIdx: index("idx_purchase_orders_org").on(table.organizationId),
|
|
56
|
+
statusIdx: index("idx_purchase_orders_status").on(table.status),
|
|
57
|
+
poNumUnique: uniqueIndex("purchase_orders_org_po_unique").on(table.organizationId, table.poNumber),
|
|
58
|
+
}));
|
|
59
|
+
export const purchaseOrderItems = pgTable("purchase_order_items", {
|
|
60
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
61
|
+
poId: uuid("po_id").references(() => purchaseOrders.id, { onDelete: "cascade" }).notNull(),
|
|
62
|
+
entityId: uuid("entity_id").notNull(),
|
|
63
|
+
variantId: uuid("variant_id"),
|
|
64
|
+
itemName: text("item_name").notNull(),
|
|
65
|
+
quantityOrdered: integer("quantity_ordered").notNull(),
|
|
66
|
+
quantityReceived: integer("quantity_received").notNull().default(0),
|
|
67
|
+
unitCost: integer("unit_cost").notNull(),
|
|
68
|
+
totalCost: integer("total_cost").notNull(),
|
|
69
|
+
notes: text("notes"),
|
|
70
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
71
|
+
}, (table) => ({
|
|
72
|
+
poIdx: index("idx_po_items_po").on(table.poId),
|
|
73
|
+
}));
|
|
74
|
+
export const goodsReceivedNotes = pgTable("goods_received_notes", {
|
|
75
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
76
|
+
organizationId: text("organization_id").notNull(),
|
|
77
|
+
grnNumber: text("grn_number").notNull(),
|
|
78
|
+
poId: uuid("po_id").references(() => purchaseOrders.id).notNull(),
|
|
79
|
+
supplierId: uuid("supplier_id").references(() => suppliers.id).notNull(),
|
|
80
|
+
warehouseId: uuid("warehouse_id").notNull(),
|
|
81
|
+
receivedBy: text("received_by").notNull(),
|
|
82
|
+
status: text("status", { enum: ["draft", "inspecting", "accepted", "accepted_with_discrepancy", "rejected"] }).notNull().default("draft"),
|
|
83
|
+
receivedAt: timestamp("received_at", { withTimezone: true }).defaultNow().notNull(),
|
|
84
|
+
notes: text("notes"),
|
|
85
|
+
metadata: jsonb("metadata").$type().default({}),
|
|
86
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
87
|
+
}, (table) => ({
|
|
88
|
+
orgIdx: index("idx_grn_org").on(table.organizationId),
|
|
89
|
+
poIdx: index("idx_grn_po").on(table.poId),
|
|
90
|
+
grnNumUnique: uniqueIndex("grn_org_number_unique").on(table.organizationId, table.grnNumber),
|
|
91
|
+
}));
|
|
92
|
+
export const grnItems = pgTable("grn_items", {
|
|
93
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
94
|
+
grnId: uuid("grn_id").references(() => goodsReceivedNotes.id, { onDelete: "cascade" }).notNull(),
|
|
95
|
+
poItemId: uuid("po_item_id").references(() => purchaseOrderItems.id).notNull(),
|
|
96
|
+
entityId: uuid("entity_id").notNull(),
|
|
97
|
+
variantId: uuid("variant_id"),
|
|
98
|
+
quantityOrdered: integer("quantity_ordered").notNull(),
|
|
99
|
+
quantityReceived: integer("quantity_received").notNull(),
|
|
100
|
+
quantityAccepted: integer("quantity_accepted").notNull(),
|
|
101
|
+
quantityRejected: integer("quantity_rejected").notNull().default(0),
|
|
102
|
+
rejectionReason: text("rejection_reason"),
|
|
103
|
+
batchNumber: text("batch_number"),
|
|
104
|
+
expiryDate: timestamp("expiry_date", { withTimezone: true }),
|
|
105
|
+
unitCost: integer("unit_cost").notNull(),
|
|
106
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
107
|
+
}, (table) => ({
|
|
108
|
+
grnIdx: index("idx_grn_items_grn").on(table.grnId),
|
|
109
|
+
}));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PluginResult } from "@porulle/core";
|
|
2
|
+
import type { Db, GoodsReceivedNote, GRNItem } from "../types.js";
|
|
3
|
+
export declare class GRNService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Db);
|
|
6
|
+
create(orgId: string, input: {
|
|
7
|
+
poId: string;
|
|
8
|
+
supplierId: string;
|
|
9
|
+
warehouseId: string;
|
|
10
|
+
receivedBy: string;
|
|
11
|
+
notes?: string;
|
|
12
|
+
items: Array<{
|
|
13
|
+
poItemId: string;
|
|
14
|
+
entityId: string;
|
|
15
|
+
variantId?: string;
|
|
16
|
+
quantityOrdered: number;
|
|
17
|
+
quantityReceived: number;
|
|
18
|
+
quantityAccepted: number;
|
|
19
|
+
quantityRejected?: number;
|
|
20
|
+
rejectionReason?: string;
|
|
21
|
+
batchNumber?: string;
|
|
22
|
+
expiryDate?: string;
|
|
23
|
+
unitCost: number;
|
|
24
|
+
}>;
|
|
25
|
+
}): Promise<PluginResult<GoodsReceivedNote>>;
|
|
26
|
+
list(orgId: string): Promise<PluginResult<GoodsReceivedNote[]>>;
|
|
27
|
+
getById(orgId: string, id: string): Promise<PluginResult<{
|
|
28
|
+
grn: GoodsReceivedNote;
|
|
29
|
+
items: GRNItem[];
|
|
30
|
+
}>>;
|
|
31
|
+
accept(orgId: string, id: string): Promise<PluginResult<GoodsReceivedNote>>;
|
|
32
|
+
private generateGRNNumber;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=grn-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grn-service.d.ts","sourceRoot":"","sources":["../../src/services/grn-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAElE,qBAAa,UAAU;IACT,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAC1F,KAAK,EAAE,KAAK,CAAC;YACX,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAC;YACvD,eAAe,EAAE,MAAM,CAAC;YAAC,gBAAgB,EAAE,MAAM,CAAC;YAAC,gBAAgB,EAAE,MAAM,CAAC;YAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;YAAC,eAAe,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAC;YAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;SACvC,CAAC,CAAC;KACJ,GAAG,OAAO,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;IA0CtC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,iBAAiB,EAAE,CAAC,CAAC;IAK/D,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,GAAG,EAAE,iBAAiB,CAAC;QAAC,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;IAQvG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;YAYnE,iBAAiB;CAMhC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { eq, and, sql } from "@porulle/core/drizzle";
|
|
2
|
+
import { Ok, Err } from "@porulle/core";
|
|
3
|
+
import { goodsReceivedNotes, grnItems, purchaseOrders, purchaseOrderItems } from "../schema.js";
|
|
4
|
+
export class GRNService {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async create(orgId, input) {
|
|
10
|
+
const grnNumber = await this.generateGRNNumber(orgId);
|
|
11
|
+
const rows = await this.db.insert(goodsReceivedNotes).values({
|
|
12
|
+
organizationId: orgId, grnNumber, poId: input.poId, supplierId: input.supplierId,
|
|
13
|
+
warehouseId: input.warehouseId, receivedBy: input.receivedBy, notes: input.notes,
|
|
14
|
+
}).returning();
|
|
15
|
+
const grn = rows[0];
|
|
16
|
+
for (const item of input.items) {
|
|
17
|
+
await this.db.insert(grnItems).values({
|
|
18
|
+
grnId: grn.id, poItemId: item.poItemId, entityId: item.entityId,
|
|
19
|
+
variantId: item.variantId, quantityOrdered: item.quantityOrdered,
|
|
20
|
+
quantityReceived: item.quantityReceived, quantityAccepted: item.quantityAccepted,
|
|
21
|
+
quantityRejected: item.quantityRejected ?? 0, rejectionReason: item.rejectionReason,
|
|
22
|
+
batchNumber: item.batchNumber,
|
|
23
|
+
...(item.expiryDate ? { expiryDate: new Date(item.expiryDate) } : {}),
|
|
24
|
+
unitCost: item.unitCost,
|
|
25
|
+
});
|
|
26
|
+
// Update PO item received quantity
|
|
27
|
+
await this.db.update(purchaseOrderItems).set({
|
|
28
|
+
quantityReceived: sql `${purchaseOrderItems.quantityReceived} + ${item.quantityReceived}`,
|
|
29
|
+
}).where(eq(purchaseOrderItems.id, item.poItemId));
|
|
30
|
+
}
|
|
31
|
+
// Check if PO is fully received
|
|
32
|
+
const poItems = await this.db.select().from(purchaseOrderItems).where(eq(purchaseOrderItems.poId, input.poId));
|
|
33
|
+
const allReceived = poItems.every(pi => pi.quantityReceived >= pi.quantityOrdered);
|
|
34
|
+
const someReceived = poItems.some(pi => pi.quantityReceived > 0);
|
|
35
|
+
if (allReceived) {
|
|
36
|
+
await this.db.update(purchaseOrders).set({ status: "received", updatedAt: new Date() })
|
|
37
|
+
.where(eq(purchaseOrders.id, input.poId));
|
|
38
|
+
}
|
|
39
|
+
else if (someReceived) {
|
|
40
|
+
await this.db.update(purchaseOrders).set({ status: "partially_received", updatedAt: new Date() })
|
|
41
|
+
.where(eq(purchaseOrders.id, input.poId));
|
|
42
|
+
}
|
|
43
|
+
return Ok(grn);
|
|
44
|
+
}
|
|
45
|
+
async list(orgId) {
|
|
46
|
+
const rows = await this.db.select().from(goodsReceivedNotes).where(eq(goodsReceivedNotes.organizationId, orgId));
|
|
47
|
+
return Ok(rows);
|
|
48
|
+
}
|
|
49
|
+
async getById(orgId, id) {
|
|
50
|
+
const rows = await this.db.select().from(goodsReceivedNotes)
|
|
51
|
+
.where(and(eq(goodsReceivedNotes.id, id), eq(goodsReceivedNotes.organizationId, orgId)));
|
|
52
|
+
if (rows.length === 0)
|
|
53
|
+
return Err("GRN not found");
|
|
54
|
+
const items = await this.db.select().from(grnItems).where(eq(grnItems.grnId, id));
|
|
55
|
+
return Ok({ grn: rows[0], items });
|
|
56
|
+
}
|
|
57
|
+
async accept(orgId, id) {
|
|
58
|
+
const items = await this.db.select().from(grnItems)
|
|
59
|
+
.where(eq(grnItems.grnId, id));
|
|
60
|
+
const hasDiscrepancy = items.some(i => i.quantityRejected > 0);
|
|
61
|
+
const newStatus = hasDiscrepancy ? "accepted_with_discrepancy" : "accepted";
|
|
62
|
+
const rows = await this.db.update(goodsReceivedNotes).set({ status: newStatus })
|
|
63
|
+
.where(and(eq(goodsReceivedNotes.id, id), eq(goodsReceivedNotes.organizationId, orgId))).returning();
|
|
64
|
+
if (rows.length === 0)
|
|
65
|
+
return Err("GRN not found");
|
|
66
|
+
return Ok(rows[0]);
|
|
67
|
+
}
|
|
68
|
+
async generateGRNNumber(orgId) {
|
|
69
|
+
const countRows = await this.db.select({ count: sql `COUNT(*)`.as("count") })
|
|
70
|
+
.from(goodsReceivedNotes).where(eq(goodsReceivedNotes.organizationId, orgId));
|
|
71
|
+
const seq = Number(countRows[0]?.count ?? 0) + 1;
|
|
72
|
+
return `GRN-${String(seq).padStart(4, "0")}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PluginResult } from "@porulle/core";
|
|
2
|
+
import type { Db, PurchaseOrder, PurchaseOrderItem } from "../types.js";
|
|
3
|
+
export declare class PurchaseOrderService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Db);
|
|
6
|
+
create(orgId: string, input: {
|
|
7
|
+
supplierId: string;
|
|
8
|
+
warehouseId: string;
|
|
9
|
+
requestedBy: string;
|
|
10
|
+
expectedDelivery?: string;
|
|
11
|
+
notes?: string;
|
|
12
|
+
items: Array<{
|
|
13
|
+
entityId: string;
|
|
14
|
+
variantId?: string;
|
|
15
|
+
itemName: string;
|
|
16
|
+
quantityOrdered: number;
|
|
17
|
+
unitCost: number;
|
|
18
|
+
}>;
|
|
19
|
+
}): Promise<PluginResult<PurchaseOrder>>;
|
|
20
|
+
list(orgId: string, status?: string): Promise<PluginResult<PurchaseOrder[]>>;
|
|
21
|
+
getById(orgId: string, id: string): Promise<PluginResult<{
|
|
22
|
+
po: PurchaseOrder;
|
|
23
|
+
items: PurchaseOrderItem[];
|
|
24
|
+
}>>;
|
|
25
|
+
submit(orgId: string, id: string): Promise<PluginResult<PurchaseOrder>>;
|
|
26
|
+
approve(orgId: string, id: string, approvedBy: string): Promise<PluginResult<PurchaseOrder>>;
|
|
27
|
+
cancel(orgId: string, id: string): Promise<PluginResult<PurchaseOrder>>;
|
|
28
|
+
private transition;
|
|
29
|
+
private generatePONumber;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=po-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"po-service.d.ts","sourceRoot":"","sources":["../../src/services/po-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAExE,qBAAa,oBAAoB;IACnB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAC1C,KAAK,EAAE,KAAK,CAAC;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,eAAe,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACrH,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IAuBlC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC,CAAC;IAO5E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,EAAE,EAAE,aAAa,CAAC;QAAC,KAAK,EAAE,iBAAiB,EAAE,CAAA;KAAE,CAAC,CAAC;IAO5G,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IAIvE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IAS5F,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;YAO/D,UAAU;YAQV,gBAAgB;CAM/B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { eq, and, sql } from "@porulle/core/drizzle";
|
|
2
|
+
import { Ok, Err } from "@porulle/core";
|
|
3
|
+
import { purchaseOrders, purchaseOrderItems } from "../schema.js";
|
|
4
|
+
export class PurchaseOrderService {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async create(orgId, input) {
|
|
10
|
+
const poNumber = await this.generatePONumber(orgId);
|
|
11
|
+
let subtotal = 0;
|
|
12
|
+
for (const item of input.items)
|
|
13
|
+
subtotal += item.quantityOrdered * item.unitCost;
|
|
14
|
+
const rows = await this.db.insert(purchaseOrders).values({
|
|
15
|
+
organizationId: orgId, poNumber, supplierId: input.supplierId,
|
|
16
|
+
warehouseId: input.warehouseId, requestedBy: input.requestedBy,
|
|
17
|
+
...(input.expectedDelivery ? { expectedDelivery: new Date(input.expectedDelivery) } : {}),
|
|
18
|
+
subtotal, grandTotal: subtotal, notes: input.notes,
|
|
19
|
+
}).returning();
|
|
20
|
+
const po = rows[0];
|
|
21
|
+
for (const item of input.items) {
|
|
22
|
+
await this.db.insert(purchaseOrderItems).values({
|
|
23
|
+
poId: po.id, entityId: item.entityId, variantId: item.variantId,
|
|
24
|
+
itemName: item.itemName, quantityOrdered: item.quantityOrdered,
|
|
25
|
+
unitCost: item.unitCost, totalCost: item.quantityOrdered * item.unitCost,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return Ok(po);
|
|
29
|
+
}
|
|
30
|
+
async list(orgId, status) {
|
|
31
|
+
const conditions = [eq(purchaseOrders.organizationId, orgId)];
|
|
32
|
+
if (status)
|
|
33
|
+
conditions.push(eq(purchaseOrders.status, status));
|
|
34
|
+
const rows = await this.db.select().from(purchaseOrders).where(and(...conditions));
|
|
35
|
+
return Ok(rows);
|
|
36
|
+
}
|
|
37
|
+
async getById(orgId, id) {
|
|
38
|
+
const rows = await this.db.select().from(purchaseOrders).where(and(eq(purchaseOrders.id, id), eq(purchaseOrders.organizationId, orgId)));
|
|
39
|
+
if (rows.length === 0)
|
|
40
|
+
return Err("Purchase order not found");
|
|
41
|
+
const items = await this.db.select().from(purchaseOrderItems).where(eq(purchaseOrderItems.poId, id));
|
|
42
|
+
return Ok({ po: rows[0], items });
|
|
43
|
+
}
|
|
44
|
+
async submit(orgId, id) {
|
|
45
|
+
return this.transition(orgId, id, "draft", "pending_approval");
|
|
46
|
+
}
|
|
47
|
+
async approve(orgId, id, approvedBy) {
|
|
48
|
+
const rows = await this.db.update(purchaseOrders).set({
|
|
49
|
+
status: "approved", approvedBy, approvedAt: new Date(), updatedAt: new Date(),
|
|
50
|
+
}).where(and(eq(purchaseOrders.id, id), eq(purchaseOrders.organizationId, orgId), eq(purchaseOrders.status, "pending_approval")))
|
|
51
|
+
.returning();
|
|
52
|
+
if (rows.length === 0)
|
|
53
|
+
return Err("PO not found or not in pending_approval status");
|
|
54
|
+
return Ok(rows[0]);
|
|
55
|
+
}
|
|
56
|
+
async cancel(orgId, id) {
|
|
57
|
+
const rows = await this.db.update(purchaseOrders).set({ status: "cancelled", updatedAt: new Date() })
|
|
58
|
+
.where(and(eq(purchaseOrders.id, id), eq(purchaseOrders.organizationId, orgId))).returning();
|
|
59
|
+
if (rows.length === 0)
|
|
60
|
+
return Err("PO not found");
|
|
61
|
+
return Ok(rows[0]);
|
|
62
|
+
}
|
|
63
|
+
async transition(orgId, id, from, to) {
|
|
64
|
+
const rows = await this.db.update(purchaseOrders).set({ status: to, updatedAt: new Date() })
|
|
65
|
+
.where(and(eq(purchaseOrders.id, id), eq(purchaseOrders.organizationId, orgId), eq(purchaseOrders.status, from)))
|
|
66
|
+
.returning();
|
|
67
|
+
if (rows.length === 0)
|
|
68
|
+
return Err(`PO not found or not in '${from}' status`);
|
|
69
|
+
return Ok(rows[0]);
|
|
70
|
+
}
|
|
71
|
+
async generatePONumber(orgId) {
|
|
72
|
+
const countRows = await this.db.select({ count: sql `COUNT(*)`.as("count") })
|
|
73
|
+
.from(purchaseOrders).where(eq(purchaseOrders.organizationId, orgId));
|
|
74
|
+
const seq = Number(countRows[0]?.count ?? 0) + 1;
|
|
75
|
+
return `PO-${String(seq).padStart(4, "0")}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PluginResult } from "@porulle/core";
|
|
2
|
+
import type { Db, Supplier, SupplierItem } from "../types.js";
|
|
3
|
+
export declare class SupplierService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Db);
|
|
6
|
+
create(orgId: string, input: {
|
|
7
|
+
name: string;
|
|
8
|
+
code: string;
|
|
9
|
+
contactName?: string;
|
|
10
|
+
contactEmail?: string;
|
|
11
|
+
contactPhone?: string;
|
|
12
|
+
paymentTermsDays?: number;
|
|
13
|
+
currency?: string;
|
|
14
|
+
}): Promise<PluginResult<Supplier>>;
|
|
15
|
+
list(orgId: string): Promise<PluginResult<Supplier[]>>;
|
|
16
|
+
getById(orgId: string, id: string): Promise<PluginResult<{
|
|
17
|
+
supplier: Supplier;
|
|
18
|
+
items: SupplierItem[];
|
|
19
|
+
}>>;
|
|
20
|
+
addItem(supplierId: string, input: {
|
|
21
|
+
entityId: string;
|
|
22
|
+
variantId?: string;
|
|
23
|
+
supplierSku?: string;
|
|
24
|
+
unitCost: number;
|
|
25
|
+
minOrderQuantity?: number;
|
|
26
|
+
leadTimeDays?: number;
|
|
27
|
+
isPreferred?: boolean;
|
|
28
|
+
}): Promise<PluginResult<SupplierItem>>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=supplier-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supplier-service.d.ts","sourceRoot":"","sources":["../../src/services/supplier-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAmB,MAAM,eAAe,CAAC;AAEnE,OAAO,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE9D,qBAAa,eAAe;IACd,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QACxE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;KACrE,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAQ7B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC;IAKtD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC,CAAC;IAOxG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE;QACvC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;KACzE,GAAG,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;CAIxC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { eq, and } from "@porulle/core/drizzle";
|
|
2
|
+
import { Ok, Err } from "@porulle/core";
|
|
3
|
+
import { suppliers, supplierItems } from "../schema.js";
|
|
4
|
+
export class SupplierService {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async create(orgId, input) {
|
|
10
|
+
const existing = await this.db.select().from(suppliers)
|
|
11
|
+
.where(and(eq(suppliers.organizationId, orgId), eq(suppliers.code, input.code)));
|
|
12
|
+
if (existing.length > 0)
|
|
13
|
+
return Err(`Supplier code '${input.code}' already exists`);
|
|
14
|
+
const rows = await this.db.insert(suppliers).values({ organizationId: orgId, ...input }).returning();
|
|
15
|
+
return Ok(rows[0]);
|
|
16
|
+
}
|
|
17
|
+
async list(orgId) {
|
|
18
|
+
const rows = await this.db.select().from(suppliers).where(eq(suppliers.organizationId, orgId));
|
|
19
|
+
return Ok(rows);
|
|
20
|
+
}
|
|
21
|
+
async getById(orgId, id) {
|
|
22
|
+
const rows = await this.db.select().from(suppliers).where(and(eq(suppliers.id, id), eq(suppliers.organizationId, orgId)));
|
|
23
|
+
if (rows.length === 0)
|
|
24
|
+
return Err("Supplier not found");
|
|
25
|
+
const items = await this.db.select().from(supplierItems).where(eq(supplierItems.supplierId, id));
|
|
26
|
+
return Ok({ supplier: rows[0], items });
|
|
27
|
+
}
|
|
28
|
+
async addItem(supplierId, input) {
|
|
29
|
+
const rows = await this.db.insert(supplierItems).values({ supplierId, ...input }).returning();
|
|
30
|
+
return Ok(rows[0]);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { PluginDb as Db } from "@porulle/core";
|
|
2
|
+
import type { suppliers, supplierItems, purchaseOrders, purchaseOrderItems, goodsReceivedNotes, grnItems } from "./schema.js";
|
|
3
|
+
export type Supplier = typeof suppliers.$inferSelect;
|
|
4
|
+
export type SupplierItem = typeof supplierItems.$inferSelect;
|
|
5
|
+
export type PurchaseOrder = typeof purchaseOrders.$inferSelect;
|
|
6
|
+
export type PurchaseOrderItem = typeof purchaseOrderItems.$inferSelect;
|
|
7
|
+
export type GoodsReceivedNote = typeof goodsReceivedNotes.$inferSelect;
|
|
8
|
+
export type GRNItem = typeof grnItems.$inferSelect;
|
|
9
|
+
export type POStatus = "draft" | "pending_approval" | "approved" | "sent" | "partially_received" | "received" | "cancelled";
|
|
10
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC9H,MAAM,MAAM,QAAQ,GAAG,OAAO,SAAS,CAAC,YAAY,CAAC;AACrD,MAAM,MAAM,YAAY,GAAG,OAAO,aAAa,CAAC,YAAY,CAAC;AAC7D,MAAM,MAAM,aAAa,GAAG,OAAO,cAAc,CAAC,YAAY,CAAC;AAC/D,MAAM,MAAM,iBAAiB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,OAAO,kBAAkB,CAAC,YAAY,CAAC;AACvE,MAAM,MAAM,OAAO,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAC;AACnD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,kBAAkB,GAAG,UAAU,GAAG,MAAM,GAAG,oBAAoB,GAAG,UAAU,GAAG,WAAW,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@porulle/plugin-procurement",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"bun": "./src/index.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./schema": {
|
|
13
|
+
"bun": "./src/schema.ts",
|
|
14
|
+
"import": "./dist/schema.js",
|
|
15
|
+
"require": "./dist/schema.js",
|
|
16
|
+
"types": "./src/schema.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
21
|
+
"check-types": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@hono/zod-openapi": "^1.2.2",
|
|
26
|
+
"@porulle/core": "workspace:*",
|
|
27
|
+
"hono": "^4.12.5"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@repo/eslint-config": "*",
|
|
31
|
+
"@repo/typescript-config": "*",
|
|
32
|
+
"@types/node": "^24.5.2",
|
|
33
|
+
"typescript": "5.9.2",
|
|
34
|
+
"vitest": "^3.2.4"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"zod": ">=4.0.0"
|
|
46
|
+
},
|
|
47
|
+
"description": "Suppliers with catalog links, purchase orders through approval, and goods-received notes for inbound receiving.",
|
|
48
|
+
"homepage": "https://porulle-docs.vercel.app",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/asyncdotengineering/porulle/issues"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/asyncdotengineering/porulle.git",
|
|
55
|
+
"directory": "packages/plugins/plugin-procurement"
|
|
56
|
+
},
|
|
57
|
+
"author": "Porulle contributors"
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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 type { Db } from "./types.js";
|
|
8
|
+
export { SupplierService } from "./services/supplier-service.js";
|
|
9
|
+
export { PurchaseOrderService } from "./services/po-service.js";
|
|
10
|
+
export { GRNService } from "./services/grn-service.js";
|
|
11
|
+
|
|
12
|
+
export function procurementPlugin() {
|
|
13
|
+
return defineCommercePlugin({
|
|
14
|
+
id: "procurement",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
permissions: [
|
|
17
|
+
{ scope: "procurement:admin", description: "Approve POs, manage suppliers, accept GRNs." },
|
|
18
|
+
{ scope: "procurement:create", description: "Create POs, create GRNs." },
|
|
19
|
+
{ scope: "procurement:read", description: "View POs, GRNs, suppliers." },
|
|
20
|
+
],
|
|
21
|
+
schema: () => ({ suppliers, supplierItems, purchaseOrders, purchaseOrderItems, goodsReceivedNotes, grnItems }),
|
|
22
|
+
hooks: () => [],
|
|
23
|
+
routes: (ctx) => {
|
|
24
|
+
const db = ctx.database.db;
|
|
25
|
+
if (!db) return [];
|
|
26
|
+
return buildProcurementRoutes(new SupplierService(db), new PurchaseOrderService(db), new GRNService(db), ctx);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|