@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.
@@ -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
+ }