@unifiedcommerce/plugin-warehouse 0.0.1
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/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/routes/warehouse.d.ts +11 -0
- package/dist/routes/warehouse.d.ts.map +1 -0
- package/dist/routes/warehouse.js +125 -0
- package/dist/schema.d.ts +1361 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +111 -0
- package/dist/services/reconciliation-service.d.ts +27 -0
- package/dist/services/reconciliation-service.d.ts.map +1 -0
- package/dist/services/reconciliation-service.js +62 -0
- package/dist/services/transfer-service.d.ts +33 -0
- package/dist/services/transfer-service.d.ts.map +1 -0
- package/dist/services/transfer-service.js +69 -0
- package/dist/services/wastage-service.d.ts +25 -0
- package/dist/services/wastage-service.d.ts.map +1 -0
- package/dist/services/wastage-service.js +42 -0
- package/dist/tsconfig.tsbuildinfo +1 -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 +37 -0
- package/src/index.ts +31 -0
- package/src/routes/warehouse.ts +134 -0
- package/src/schema.ts +118 -0
- package/src/services/reconciliation-service.ts +69 -0
- package/src/services/transfer-service.ts +74 -0
- package/src/services/wastage-service.ts +50 -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,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAcvB,CAAC;AAEJ,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoBxB,CAAC;AAEJ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAa5B,CAAC;AAEJ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EActB,CAAC;AAEJ,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAc1B,CAAC;AAEJ,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAa9B,CAAC;AAEJ,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe7B,CAAC"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { pgTable, uuid, text, integer, boolean, timestamp, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
|
|
2
|
+
export const warehouseBins = pgTable("warehouse_bins", {
|
|
3
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
4
|
+
organizationId: text("organization_id").notNull(),
|
|
5
|
+
warehouseId: uuid("warehouse_id").notNull(),
|
|
6
|
+
code: text("code").notNull(),
|
|
7
|
+
name: text("name").notNull(),
|
|
8
|
+
type: text("type", { enum: ["general", "cold", "frozen", "dry", "hazardous", "display"] }).notNull().default("general"),
|
|
9
|
+
isActive: boolean("is_active").notNull().default(true),
|
|
10
|
+
capacity: integer("capacity"),
|
|
11
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
12
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
13
|
+
}, (table) => ({
|
|
14
|
+
orgIdx: index("idx_warehouse_bins_org").on(table.organizationId),
|
|
15
|
+
codeUnique: uniqueIndex("warehouse_bins_org_wh_code_unique").on(table.organizationId, table.warehouseId, table.code),
|
|
16
|
+
}));
|
|
17
|
+
export const stockTransfers = pgTable("stock_transfers", {
|
|
18
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
19
|
+
organizationId: text("organization_id").notNull(),
|
|
20
|
+
transferNumber: text("transfer_number").notNull(),
|
|
21
|
+
type: text("type", { enum: ["requisition", "direct", "return"] }).notNull().default("requisition"),
|
|
22
|
+
status: text("status", { enum: ["draft", "pending_approval", "approved", "in_transit", "received", "cancelled"] }).notNull().default("draft"),
|
|
23
|
+
fromWarehouseId: uuid("from_warehouse_id").notNull(),
|
|
24
|
+
toWarehouseId: uuid("to_warehouse_id").notNull(),
|
|
25
|
+
requestedBy: text("requested_by").notNull(),
|
|
26
|
+
approvedBy: text("approved_by"),
|
|
27
|
+
dispatchedAt: timestamp("dispatched_at", { withTimezone: true }),
|
|
28
|
+
receivedAt: timestamp("received_at", { withTimezone: true }),
|
|
29
|
+
notes: text("notes"),
|
|
30
|
+
metadata: jsonb("metadata").$type().default({}),
|
|
31
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
32
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
33
|
+
}, (table) => ({
|
|
34
|
+
orgIdx: index("idx_stock_transfers_org").on(table.organizationId),
|
|
35
|
+
statusIdx: index("idx_stock_transfers_status").on(table.status),
|
|
36
|
+
numUnique: uniqueIndex("stock_transfers_org_num_unique").on(table.organizationId, table.transferNumber),
|
|
37
|
+
}));
|
|
38
|
+
export const stockTransferItems = pgTable("stock_transfer_items", {
|
|
39
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
40
|
+
transferId: uuid("transfer_id").references(() => stockTransfers.id, { onDelete: "cascade" }).notNull(),
|
|
41
|
+
entityId: uuid("entity_id").notNull(),
|
|
42
|
+
variantId: uuid("variant_id"),
|
|
43
|
+
itemName: text("item_name").notNull(),
|
|
44
|
+
quantityRequested: integer("quantity_requested").notNull(),
|
|
45
|
+
quantityDispatched: integer("quantity_dispatched").notNull().default(0),
|
|
46
|
+
quantityReceived: integer("quantity_received").notNull().default(0),
|
|
47
|
+
batchNumber: text("batch_number"),
|
|
48
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
49
|
+
}, (table) => ({
|
|
50
|
+
transferIdx: index("idx_stock_transfer_items_transfer").on(table.transferId),
|
|
51
|
+
}));
|
|
52
|
+
export const wastageNotes = pgTable("wastage_notes", {
|
|
53
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
54
|
+
organizationId: text("organization_id").notNull(),
|
|
55
|
+
noteNumber: text("note_number").notNull(),
|
|
56
|
+
warehouseId: uuid("warehouse_id").notNull(),
|
|
57
|
+
type: text("type", { enum: ["spoilage", "damage", "expiry", "theft", "prep_waste", "other"] }).notNull(),
|
|
58
|
+
recordedBy: text("recorded_by").notNull(),
|
|
59
|
+
approvedBy: text("approved_by"),
|
|
60
|
+
totalCost: integer("total_cost").notNull().default(0),
|
|
61
|
+
notes: text("notes"),
|
|
62
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
63
|
+
}, (table) => ({
|
|
64
|
+
orgIdx: index("idx_wastage_notes_org").on(table.organizationId),
|
|
65
|
+
numUnique: uniqueIndex("wastage_notes_org_num_unique").on(table.organizationId, table.noteNumber),
|
|
66
|
+
}));
|
|
67
|
+
export const wastageNoteItems = pgTable("wastage_note_items", {
|
|
68
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
69
|
+
noteId: uuid("note_id").references(() => wastageNotes.id, { onDelete: "cascade" }).notNull(),
|
|
70
|
+
entityId: uuid("entity_id").notNull(),
|
|
71
|
+
variantId: uuid("variant_id"),
|
|
72
|
+
itemName: text("item_name").notNull(),
|
|
73
|
+
quantity: integer("quantity").notNull(),
|
|
74
|
+
unitCost: integer("unit_cost").notNull(),
|
|
75
|
+
totalCost: integer("total_cost").notNull(),
|
|
76
|
+
reason: text("reason"),
|
|
77
|
+
batchNumber: text("batch_number"),
|
|
78
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
79
|
+
}, (table) => ({
|
|
80
|
+
noteIdx: index("idx_wastage_note_items_note").on(table.noteId),
|
|
81
|
+
}));
|
|
82
|
+
export const stockReconciliations = pgTable("stock_reconciliations", {
|
|
83
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
84
|
+
organizationId: text("organization_id").notNull(),
|
|
85
|
+
reconciliationNumber: text("reconciliation_number").notNull(),
|
|
86
|
+
warehouseId: uuid("warehouse_id").notNull(),
|
|
87
|
+
status: text("status", { enum: ["draft", "counting", "submitted", "approved", "adjusted"] }).notNull().default("draft"),
|
|
88
|
+
countedBy: text("counted_by").notNull(),
|
|
89
|
+
approvedBy: text("approved_by"),
|
|
90
|
+
countedAt: timestamp("counted_at", { withTimezone: true }),
|
|
91
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
92
|
+
}, (table) => ({
|
|
93
|
+
orgIdx: index("idx_stock_reconciliations_org").on(table.organizationId),
|
|
94
|
+
numUnique: uniqueIndex("stock_reconciliations_org_num_unique").on(table.organizationId, table.reconciliationNumber),
|
|
95
|
+
}));
|
|
96
|
+
export const reconciliationItems = pgTable("reconciliation_items", {
|
|
97
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
98
|
+
reconciliationId: uuid("reconciliation_id").references(() => stockReconciliations.id, { onDelete: "cascade" }).notNull(),
|
|
99
|
+
entityId: uuid("entity_id").notNull(),
|
|
100
|
+
variantId: uuid("variant_id"),
|
|
101
|
+
itemName: text("item_name").notNull(),
|
|
102
|
+
systemQuantity: integer("system_quantity").notNull(),
|
|
103
|
+
physicalQuantity: integer("physical_quantity").notNull(),
|
|
104
|
+
variance: integer("variance").notNull(),
|
|
105
|
+
varianceCost: integer("variance_cost").notNull().default(0),
|
|
106
|
+
adjustmentMade: boolean("adjustment_made").notNull().default(false),
|
|
107
|
+
notes: text("notes"),
|
|
108
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
109
|
+
}, (table) => ({
|
|
110
|
+
recIdx: index("idx_reconciliation_items_rec").on(table.reconciliationId),
|
|
111
|
+
}));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PluginResult } from "@unifiedcommerce/core";
|
|
2
|
+
import type { Db, StockReconciliation, ReconciliationItem } from "../types";
|
|
3
|
+
export declare class ReconciliationService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Db);
|
|
6
|
+
create(orgId: string, input: {
|
|
7
|
+
warehouseId: string;
|
|
8
|
+
countedBy: string;
|
|
9
|
+
items: Array<{
|
|
10
|
+
entityId: string;
|
|
11
|
+
variantId?: string;
|
|
12
|
+
itemName: string;
|
|
13
|
+
systemQuantity: number;
|
|
14
|
+
physicalQuantity: number;
|
|
15
|
+
notes?: string;
|
|
16
|
+
}>;
|
|
17
|
+
}): Promise<PluginResult<StockReconciliation>>;
|
|
18
|
+
list(orgId: string): Promise<PluginResult<StockReconciliation[]>>;
|
|
19
|
+
getById(orgId: string, id: string): Promise<PluginResult<{
|
|
20
|
+
reconciliation: StockReconciliation;
|
|
21
|
+
items: ReconciliationItem[];
|
|
22
|
+
}>>;
|
|
23
|
+
submit(orgId: string, id: string): Promise<PluginResult<StockReconciliation>>;
|
|
24
|
+
approve(orgId: string, id: string, approvedBy: string): Promise<PluginResult<StockReconciliation>>;
|
|
25
|
+
private generateNumber;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=reconciliation-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciliation-service.d.ts","sourceRoot":"","sources":["../../src/services/reconciliation-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE5E,qBAAa,qBAAqB;IACpB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QACvC,KAAK,EAAE,KAAK,CAAC;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,cAAc,EAAE,MAAM,CAAC;YAAC,gBAAgB,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC5I,GAAG,OAAO,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAC;IAmBxC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,mBAAmB,EAAE,CAAC,CAAC;IAIjE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,cAAc,EAAE,mBAAmB,CAAC;QAAC,KAAK,EAAE,kBAAkB,EAAE,CAAA;KAAE,CAAC,CAAC;IAQ/H,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAC;IAO7E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAC;YAc1F,cAAc;CAI7B"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { eq, and, sql } from "drizzle-orm";
|
|
2
|
+
import { Ok, Err } from "@unifiedcommerce/core";
|
|
3
|
+
import { stockReconciliations, reconciliationItems } from "../schema";
|
|
4
|
+
export class ReconciliationService {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async create(orgId, input) {
|
|
10
|
+
const recNumber = await this.generateNumber(orgId);
|
|
11
|
+
const rows = await this.db.insert(stockReconciliations).values({
|
|
12
|
+
organizationId: orgId, reconciliationNumber: recNumber,
|
|
13
|
+
warehouseId: input.warehouseId, countedBy: input.countedBy, countedAt: new Date(),
|
|
14
|
+
}).returning();
|
|
15
|
+
const rec = rows[0];
|
|
16
|
+
for (const item of input.items) {
|
|
17
|
+
const variance = item.physicalQuantity - item.systemQuantity;
|
|
18
|
+
await this.db.insert(reconciliationItems).values({
|
|
19
|
+
reconciliationId: rec.id, entityId: item.entityId, variantId: item.variantId,
|
|
20
|
+
itemName: item.itemName, systemQuantity: item.systemQuantity,
|
|
21
|
+
physicalQuantity: item.physicalQuantity, variance, notes: item.notes,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return Ok(rec);
|
|
25
|
+
}
|
|
26
|
+
async list(orgId) {
|
|
27
|
+
return Ok(await this.db.select().from(stockReconciliations).where(eq(stockReconciliations.organizationId, orgId)));
|
|
28
|
+
}
|
|
29
|
+
async getById(orgId, id) {
|
|
30
|
+
const rows = await this.db.select().from(stockReconciliations)
|
|
31
|
+
.where(and(eq(stockReconciliations.id, id), eq(stockReconciliations.organizationId, orgId)));
|
|
32
|
+
if (rows.length === 0)
|
|
33
|
+
return Err("Reconciliation not found");
|
|
34
|
+
const items = await this.db.select().from(reconciliationItems).where(eq(reconciliationItems.reconciliationId, id));
|
|
35
|
+
return Ok({ reconciliation: rows[0], items });
|
|
36
|
+
}
|
|
37
|
+
async submit(orgId, id) {
|
|
38
|
+
const rows = await this.db.update(stockReconciliations).set({ status: "submitted" })
|
|
39
|
+
.where(and(eq(stockReconciliations.id, id), eq(stockReconciliations.organizationId, orgId), eq(stockReconciliations.status, "draft"))).returning();
|
|
40
|
+
if (rows.length === 0)
|
|
41
|
+
return Err("Not found or not in draft status");
|
|
42
|
+
return Ok(rows[0]);
|
|
43
|
+
}
|
|
44
|
+
async approve(orgId, id, approvedBy) {
|
|
45
|
+
// Mark items with variance as adjusted
|
|
46
|
+
const items = await this.db.select().from(reconciliationItems).where(eq(reconciliationItems.reconciliationId, id));
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
if (item.variance !== 0) {
|
|
49
|
+
await this.db.update(reconciliationItems).set({ adjustmentMade: true }).where(eq(reconciliationItems.id, item.id));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const rows = await this.db.update(stockReconciliations).set({ status: "approved", approvedBy })
|
|
53
|
+
.where(and(eq(stockReconciliations.id, id), eq(stockReconciliations.organizationId, orgId), eq(stockReconciliations.status, "submitted"))).returning();
|
|
54
|
+
if (rows.length === 0)
|
|
55
|
+
return Err("Not found or not in submitted status");
|
|
56
|
+
return Ok(rows[0]);
|
|
57
|
+
}
|
|
58
|
+
async generateNumber(orgId) {
|
|
59
|
+
const countRows = await this.db.select({ count: sql `COUNT(*)`.as("count") }).from(stockReconciliations).where(eq(stockReconciliations.organizationId, orgId));
|
|
60
|
+
return `REC-${String(Number(countRows[0]?.count ?? 0) + 1).padStart(4, "0")}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PluginResult } from "@unifiedcommerce/core";
|
|
2
|
+
import type { Db, StockTransfer, StockTransferItem } from "../types";
|
|
3
|
+
export declare class TransferService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Db);
|
|
6
|
+
create(orgId: string, input: {
|
|
7
|
+
fromWarehouseId: string;
|
|
8
|
+
toWarehouseId: string;
|
|
9
|
+
requestedBy: string;
|
|
10
|
+
type?: "requisition" | "direct" | "return";
|
|
11
|
+
notes?: string;
|
|
12
|
+
items: Array<{
|
|
13
|
+
entityId: string;
|
|
14
|
+
variantId?: string;
|
|
15
|
+
itemName: string;
|
|
16
|
+
quantityRequested: number;
|
|
17
|
+
batchNumber?: string;
|
|
18
|
+
}>;
|
|
19
|
+
}): Promise<PluginResult<StockTransfer>>;
|
|
20
|
+
list(orgId: string, status?: string): Promise<PluginResult<StockTransfer[]>>;
|
|
21
|
+
getById(orgId: string, id: string): Promise<PluginResult<{
|
|
22
|
+
transfer: StockTransfer;
|
|
23
|
+
items: StockTransferItem[];
|
|
24
|
+
}>>;
|
|
25
|
+
approve(orgId: string, id: string, approvedBy: string): Promise<PluginResult<StockTransfer>>;
|
|
26
|
+
dispatch(orgId: string, id: string): Promise<PluginResult<StockTransfer>>;
|
|
27
|
+
receive(orgId: string, id: string, receivedItems: Array<{
|
|
28
|
+
itemId: string;
|
|
29
|
+
quantityReceived: number;
|
|
30
|
+
}>): Promise<PluginResult<StockTransfer>>;
|
|
31
|
+
private generateNumber;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=transfer-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transfer-service.d.ts","sourceRoot":"","sources":["../../src/services/transfer-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAErE,qBAAa,eAAe;IACd,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,eAAe,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QACpE,IAAI,CAAC,EAAE,aAAa,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAC3D,KAAK,EAAE,KAAK,CAAC;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,iBAAiB,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC3H,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IAelC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC,CAAC;IAM5E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,QAAQ,EAAE,aAAa,CAAC;QAAC,KAAK,EAAE,iBAAiB,EAAE,CAAA;KAAE,CAAC,CAAC;IAOlH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IAO5F,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IAWzE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;YAUpI,cAAc;CAI7B"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { eq, and, sql } from "drizzle-orm";
|
|
2
|
+
import { Ok, Err } from "@unifiedcommerce/core";
|
|
3
|
+
import { stockTransfers, stockTransferItems } from "../schema";
|
|
4
|
+
export class TransferService {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async create(orgId, input) {
|
|
10
|
+
if (input.fromWarehouseId === input.toWarehouseId)
|
|
11
|
+
return Err("Cannot transfer to the same warehouse");
|
|
12
|
+
const transferNumber = await this.generateNumber(orgId, "TRF");
|
|
13
|
+
const rows = await this.db.insert(stockTransfers).values({
|
|
14
|
+
organizationId: orgId, transferNumber, type: input.type ?? "requisition",
|
|
15
|
+
fromWarehouseId: input.fromWarehouseId, toWarehouseId: input.toWarehouseId,
|
|
16
|
+
requestedBy: input.requestedBy, notes: input.notes,
|
|
17
|
+
}).returning();
|
|
18
|
+
const transfer = rows[0];
|
|
19
|
+
for (const item of input.items) {
|
|
20
|
+
await this.db.insert(stockTransferItems).values({ transferId: transfer.id, ...item });
|
|
21
|
+
}
|
|
22
|
+
return Ok(transfer);
|
|
23
|
+
}
|
|
24
|
+
async list(orgId, status) {
|
|
25
|
+
const conditions = [eq(stockTransfers.organizationId, orgId)];
|
|
26
|
+
if (status)
|
|
27
|
+
conditions.push(eq(stockTransfers.status, status));
|
|
28
|
+
return Ok(await this.db.select().from(stockTransfers).where(and(...conditions)));
|
|
29
|
+
}
|
|
30
|
+
async getById(orgId, id) {
|
|
31
|
+
const rows = await this.db.select().from(stockTransfers).where(and(eq(stockTransfers.id, id), eq(stockTransfers.organizationId, orgId)));
|
|
32
|
+
if (rows.length === 0)
|
|
33
|
+
return Err("Transfer not found");
|
|
34
|
+
const items = await this.db.select().from(stockTransferItems).where(eq(stockTransferItems.transferId, id));
|
|
35
|
+
return Ok({ transfer: rows[0], items });
|
|
36
|
+
}
|
|
37
|
+
async approve(orgId, id, approvedBy) {
|
|
38
|
+
const rows = await this.db.update(stockTransfers).set({ status: "approved", approvedBy, updatedAt: new Date() })
|
|
39
|
+
.where(and(eq(stockTransfers.id, id), eq(stockTransfers.organizationId, orgId), eq(stockTransfers.status, "draft"))).returning();
|
|
40
|
+
if (rows.length === 0)
|
|
41
|
+
return Err("Transfer not found or not in draft status");
|
|
42
|
+
return Ok(rows[0]);
|
|
43
|
+
}
|
|
44
|
+
async dispatch(orgId, id) {
|
|
45
|
+
const items = await this.db.select().from(stockTransferItems).where(eq(stockTransferItems.transferId, id));
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
await this.db.update(stockTransferItems).set({ quantityDispatched: item.quantityRequested }).where(eq(stockTransferItems.id, item.id));
|
|
48
|
+
}
|
|
49
|
+
const rows = await this.db.update(stockTransfers).set({ status: "in_transit", dispatchedAt: new Date(), updatedAt: new Date() })
|
|
50
|
+
.where(and(eq(stockTransfers.id, id), eq(stockTransfers.organizationId, orgId), eq(stockTransfers.status, "approved"))).returning();
|
|
51
|
+
if (rows.length === 0)
|
|
52
|
+
return Err("Transfer not found or not in approved status");
|
|
53
|
+
return Ok(rows[0]);
|
|
54
|
+
}
|
|
55
|
+
async receive(orgId, id, receivedItems) {
|
|
56
|
+
for (const ri of receivedItems) {
|
|
57
|
+
await this.db.update(stockTransferItems).set({ quantityReceived: ri.quantityReceived }).where(eq(stockTransferItems.id, ri.itemId));
|
|
58
|
+
}
|
|
59
|
+
const rows = await this.db.update(stockTransfers).set({ status: "received", receivedAt: new Date(), updatedAt: new Date() })
|
|
60
|
+
.where(and(eq(stockTransfers.id, id), eq(stockTransfers.organizationId, orgId), eq(stockTransfers.status, "in_transit"))).returning();
|
|
61
|
+
if (rows.length === 0)
|
|
62
|
+
return Err("Transfer not found or not in transit");
|
|
63
|
+
return Ok(rows[0]);
|
|
64
|
+
}
|
|
65
|
+
async generateNumber(orgId, prefix) {
|
|
66
|
+
const countRows = await this.db.select({ count: sql `COUNT(*)`.as("count") }).from(stockTransfers).where(eq(stockTransfers.organizationId, orgId));
|
|
67
|
+
return `${prefix}-${String(Number(countRows[0]?.count ?? 0) + 1).padStart(4, "0")}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PluginResult } from "@unifiedcommerce/core";
|
|
2
|
+
import type { Db, WastageNote } from "../types";
|
|
3
|
+
export declare class WastageService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Db);
|
|
6
|
+
create(orgId: string, input: {
|
|
7
|
+
warehouseId: string;
|
|
8
|
+
type: "spoilage" | "damage" | "expiry" | "theft" | "prep_waste" | "other";
|
|
9
|
+
recordedBy: string;
|
|
10
|
+
notes?: string;
|
|
11
|
+
items: Array<{
|
|
12
|
+
entityId: string;
|
|
13
|
+
variantId?: string;
|
|
14
|
+
itemName: string;
|
|
15
|
+
quantity: number;
|
|
16
|
+
unitCost: number;
|
|
17
|
+
reason?: string;
|
|
18
|
+
batchNumber?: string;
|
|
19
|
+
}>;
|
|
20
|
+
}): Promise<PluginResult<WastageNote>>;
|
|
21
|
+
list(orgId: string): Promise<PluginResult<WastageNote[]>>;
|
|
22
|
+
approve(orgId: string, id: string, approvedBy: string): Promise<PluginResult<WastageNote>>;
|
|
23
|
+
private generateNumber;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=wastage-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wastage-service.d.ts","sourceRoot":"","sources":["../../src/services/wastage-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,WAAW,EAAmB,MAAM,UAAU,CAAC;AAEjE,qBAAa,cAAc;IACb,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,GAAG,OAAO,CAAC;QAC/F,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QACnC,KAAK,EAAE,KAAK,CAAC;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACrJ,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAqBhC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,CAAC;IAIzD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;YAOlF,cAAc;CAI7B"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { eq, and, sql } from "drizzle-orm";
|
|
2
|
+
import { Ok, Err } from "@unifiedcommerce/core";
|
|
3
|
+
import { wastageNotes, wastageNoteItems } from "../schema";
|
|
4
|
+
export class WastageService {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async create(orgId, input) {
|
|
10
|
+
let totalCost = 0;
|
|
11
|
+
for (const item of input.items)
|
|
12
|
+
totalCost += item.quantity * item.unitCost;
|
|
13
|
+
const noteNumber = await this.generateNumber(orgId);
|
|
14
|
+
const rows = await this.db.insert(wastageNotes).values({
|
|
15
|
+
organizationId: orgId, noteNumber, warehouseId: input.warehouseId,
|
|
16
|
+
type: input.type, recordedBy: input.recordedBy, totalCost, notes: input.notes,
|
|
17
|
+
}).returning();
|
|
18
|
+
const note = rows[0];
|
|
19
|
+
for (const item of input.items) {
|
|
20
|
+
await this.db.insert(wastageNoteItems).values({
|
|
21
|
+
noteId: note.id, entityId: item.entityId, variantId: item.variantId,
|
|
22
|
+
itemName: item.itemName, quantity: item.quantity, unitCost: item.unitCost,
|
|
23
|
+
totalCost: item.quantity * item.unitCost, reason: item.reason, batchNumber: item.batchNumber,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return Ok(note);
|
|
27
|
+
}
|
|
28
|
+
async list(orgId) {
|
|
29
|
+
return Ok(await this.db.select().from(wastageNotes).where(eq(wastageNotes.organizationId, orgId)));
|
|
30
|
+
}
|
|
31
|
+
async approve(orgId, id, approvedBy) {
|
|
32
|
+
const rows = await this.db.update(wastageNotes).set({ approvedBy })
|
|
33
|
+
.where(and(eq(wastageNotes.id, id), eq(wastageNotes.organizationId, orgId))).returning();
|
|
34
|
+
if (rows.length === 0)
|
|
35
|
+
return Err("Wastage note not found");
|
|
36
|
+
return Ok(rows[0]);
|
|
37
|
+
}
|
|
38
|
+
async generateNumber(orgId) {
|
|
39
|
+
const countRows = await this.db.select({ count: sql `COUNT(*)`.as("count") }).from(wastageNotes).where(eq(wastageNotes.organizationId, orgId));
|
|
40
|
+
return `WST-${String(Number(countRows[0]?.count ?? 0) + 1).padStart(4, "0")}`;
|
|
41
|
+
}
|
|
42
|
+
}
|