@unifiedcommerce/plugin-pos 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.
Files changed (77) hide show
  1. package/dist/hooks/checkout-pos.d.ts +29 -0
  2. package/dist/hooks/checkout-pos.d.ts.map +1 -0
  3. package/dist/hooks/checkout-pos.js +69 -0
  4. package/dist/index.d.ts +26 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +109 -0
  7. package/dist/payment-adapter.d.ts +33 -0
  8. package/dist/payment-adapter.d.ts.map +1 -0
  9. package/dist/payment-adapter.js +37 -0
  10. package/dist/routes/lookup.d.ts +9 -0
  11. package/dist/routes/lookup.d.ts.map +1 -0
  12. package/dist/routes/lookup.js +37 -0
  13. package/dist/routes/payments.d.ts +10 -0
  14. package/dist/routes/payments.d.ts.map +1 -0
  15. package/dist/routes/payments.js +62 -0
  16. package/dist/routes/receipts.d.ts +9 -0
  17. package/dist/routes/receipts.d.ts.map +1 -0
  18. package/dist/routes/receipts.js +28 -0
  19. package/dist/routes/returns.d.ts +21 -0
  20. package/dist/routes/returns.d.ts.map +1 -0
  21. package/dist/routes/returns.js +83 -0
  22. package/dist/routes/shifts.d.ts +9 -0
  23. package/dist/routes/shifts.d.ts.map +1 -0
  24. package/dist/routes/shifts.js +91 -0
  25. package/dist/routes/terminals.d.ts +9 -0
  26. package/dist/routes/terminals.d.ts.map +1 -0
  27. package/dist/routes/terminals.js +55 -0
  28. package/dist/routes/transactions.d.ts +19 -0
  29. package/dist/routes/transactions.d.ts.map +1 -0
  30. package/dist/routes/transactions.js +175 -0
  31. package/dist/schema.d.ts +1337 -0
  32. package/dist/schema.d.ts.map +1 -0
  33. package/dist/schema.js +123 -0
  34. package/dist/services/lookup-service.d.ts +38 -0
  35. package/dist/services/lookup-service.d.ts.map +1 -0
  36. package/dist/services/lookup-service.js +104 -0
  37. package/dist/services/payment-service.d.ts +40 -0
  38. package/dist/services/payment-service.d.ts.map +1 -0
  39. package/dist/services/payment-service.js +99 -0
  40. package/dist/services/receipt-service.d.ts +45 -0
  41. package/dist/services/receipt-service.d.ts.map +1 -0
  42. package/dist/services/receipt-service.js +119 -0
  43. package/dist/services/return-service.d.ts +27 -0
  44. package/dist/services/return-service.d.ts.map +1 -0
  45. package/dist/services/return-service.js +51 -0
  46. package/dist/services/shift-service.d.ts +36 -0
  47. package/dist/services/shift-service.d.ts.map +1 -0
  48. package/dist/services/shift-service.js +198 -0
  49. package/dist/services/terminal-service.d.ts +21 -0
  50. package/dist/services/terminal-service.d.ts.map +1 -0
  51. package/dist/services/terminal-service.js +59 -0
  52. package/dist/services/transaction-service.d.ts +30 -0
  53. package/dist/services/transaction-service.d.ts.map +1 -0
  54. package/dist/services/transaction-service.js +202 -0
  55. package/dist/types.d.ts +30 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +5 -0
  58. package/package.json +40 -0
  59. package/src/hooks/checkout-pos.ts +93 -0
  60. package/src/index.ts +131 -0
  61. package/src/payment-adapter.ts +53 -0
  62. package/src/routes/lookup.ts +44 -0
  63. package/src/routes/payments.ts +82 -0
  64. package/src/routes/receipts.ts +35 -0
  65. package/src/routes/returns.ts +116 -0
  66. package/src/routes/shifts.ts +100 -0
  67. package/src/routes/terminals.ts +62 -0
  68. package/src/routes/transactions.ts +192 -0
  69. package/src/schema.ts +136 -0
  70. package/src/services/lookup-service.ts +136 -0
  71. package/src/services/payment-service.ts +133 -0
  72. package/src/services/receipt-service.ts +169 -0
  73. package/src/services/return-service.ts +65 -0
  74. package/src/services/shift-service.ts +260 -0
  75. package/src/services/terminal-service.ts +76 -0
  76. package/src/services/transaction-service.ts +248 -0
  77. package/src/types.ts +49 -0
@@ -0,0 +1,51 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { Ok, Err } from "@unifiedcommerce/core";
3
+ import { posReturnItems } from "../schema";
4
+ export class ReturnService {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ /**
10
+ * Record return items linking back to an original order's line items.
11
+ * Called after a return transaction is created.
12
+ */
13
+ async addReturnItems(transactionId, items) {
14
+ if (items.length === 0)
15
+ return Err("At least one item is required");
16
+ const values = items.map((item) => ({
17
+ transactionId,
18
+ originalOrderId: item.originalOrderId,
19
+ originalLineItemId: item.originalLineItemId,
20
+ quantity: item.quantity,
21
+ reason: item.reason,
22
+ restockingFee: item.restockingFee ?? 0,
23
+ refundAmount: item.refundAmount,
24
+ }));
25
+ const rows = await this.db
26
+ .insert(posReturnItems)
27
+ .values(values)
28
+ .returning();
29
+ return Ok(rows);
30
+ }
31
+ /**
32
+ * Get all return items for a return transaction.
33
+ */
34
+ async getReturnItems(transactionId) {
35
+ const rows = await this.db
36
+ .select()
37
+ .from(posReturnItems)
38
+ .where(eq(posReturnItems.transactionId, transactionId));
39
+ return Ok(rows);
40
+ }
41
+ /**
42
+ * Calculate total refund amount for a return transaction.
43
+ */
44
+ async calculateRefundTotal(transactionId) {
45
+ const items = await this.db
46
+ .select()
47
+ .from(posReturnItems)
48
+ .where(eq(posReturnItems.transactionId, transactionId));
49
+ return items.reduce((sum, item) => sum + item.refundAmount, 0);
50
+ }
51
+ }
@@ -0,0 +1,36 @@
1
+ import type { PluginResult } from "@unifiedcommerce/core";
2
+ import type { Db, Shift, CashEvent } from "../types";
3
+ export declare class ShiftService {
4
+ private db;
5
+ private transaction;
6
+ constructor(db: Db, transaction: (fn: (tx: Db) => Promise<unknown>) => Promise<unknown>);
7
+ open(orgId: string, input: {
8
+ terminalId: string;
9
+ operatorId: string;
10
+ openingFloat: number;
11
+ }): Promise<PluginResult<Shift>>;
12
+ close(orgId: string, shiftId: string, input: {
13
+ closingCount: number;
14
+ }): Promise<PluginResult<Shift>>;
15
+ getCurrent(orgId: string, operatorId: string): Promise<PluginResult<Shift | null>>;
16
+ getById(orgId: string, id: string): Promise<PluginResult<Shift>>;
17
+ addCashEvent(shiftId: string, input: {
18
+ type: "drop" | "pickup" | "paid_in" | "paid_out";
19
+ amount: number;
20
+ reason?: string;
21
+ performedBy: string;
22
+ }): Promise<PluginResult<CashEvent>>;
23
+ listCashEvents(shiftId: string): Promise<PluginResult<CashEvent[]>>;
24
+ getReport(orgId: string, shiftId: string): Promise<PluginResult<{
25
+ shift: Shift;
26
+ cashEvents: CashEvent[];
27
+ paymentMethodTotals: Array<{
28
+ method: string;
29
+ total: number;
30
+ count: number;
31
+ }>;
32
+ transactionCount: number;
33
+ }>>;
34
+ private calculateExpectedCash;
35
+ }
36
+ //# sourceMappingURL=shift-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shift-service.d.ts","sourceRoot":"","sources":["../../src/services/shift-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErD,qBAAa,YAAY;IAErB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;gBADX,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC;IAGvE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QAC/B,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAyC1B,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE;QACjD,YAAY,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAmC1B,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAalF,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAYhE,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE;QACzC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;QACjD,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;IA2B9B,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,CAAC;IAWnE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QACpE,KAAK,EAAE,KAAK,CAAC;QACb,UAAU,EAAE,SAAS,EAAE,CAAC;QACxB,mBAAmB,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAC7E,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC,CAAC;YAgDW,qBAAqB;CA4CpC"}
@@ -0,0 +1,198 @@
1
+ import { eq, and, desc, sql } from "drizzle-orm";
2
+ import { Ok, Err } from "@unifiedcommerce/core";
3
+ import { posShifts, posCashEvents, posPayments, posTransactions } from "../schema";
4
+ export class ShiftService {
5
+ db;
6
+ transaction;
7
+ constructor(db, transaction) {
8
+ this.db = db;
9
+ this.transaction = transaction;
10
+ }
11
+ async open(orgId, input) {
12
+ if (input.openingFloat < 0)
13
+ return Err("Opening float must be non-negative");
14
+ // Check no open shift on this terminal
15
+ const openShifts = await this.db
16
+ .select()
17
+ .from(posShifts)
18
+ .where(and(eq(posShifts.terminalId, input.terminalId), eq(posShifts.status, "open")));
19
+ if (openShifts.length > 0) {
20
+ return Err("Terminal already has an open shift");
21
+ }
22
+ const rows = await this.db
23
+ .insert(posShifts)
24
+ .values({
25
+ organizationId: orgId,
26
+ terminalId: input.terminalId,
27
+ operatorId: input.operatorId,
28
+ openingFloat: input.openingFloat,
29
+ status: "open",
30
+ })
31
+ .returning();
32
+ const shift = rows[0];
33
+ // Record the opening float as a cash event
34
+ await this.db.insert(posCashEvents).values({
35
+ shiftId: shift.id,
36
+ type: "float",
37
+ amount: input.openingFloat,
38
+ performedBy: input.operatorId,
39
+ performedAt: new Date(),
40
+ });
41
+ return Ok(shift);
42
+ }
43
+ async close(orgId, shiftId, input) {
44
+ const result = await this.transaction(async (tx) => {
45
+ const shifts = await tx
46
+ .select()
47
+ .from(posShifts)
48
+ .where(and(eq(posShifts.id, shiftId), eq(posShifts.organizationId, orgId)))
49
+ .for("update");
50
+ if (shifts.length === 0)
51
+ return Err("Shift not found");
52
+ const shift = shifts[0];
53
+ if (shift.status === "closed")
54
+ return Err("Shift is already closed");
55
+ // Calculate expected cash
56
+ const expectedCash = await this.calculateExpectedCash(tx, shiftId, shift.openingFloat);
57
+ const cashVariance = input.closingCount - expectedCash;
58
+ const updated = await tx
59
+ .update(posShifts)
60
+ .set({
61
+ status: "closed",
62
+ closingCount: input.closingCount,
63
+ expectedCash,
64
+ cashVariance,
65
+ closedAt: new Date(),
66
+ updatedAt: new Date(),
67
+ })
68
+ .where(eq(posShifts.id, shiftId))
69
+ .returning();
70
+ return Ok(updated[0]);
71
+ });
72
+ return result;
73
+ }
74
+ async getCurrent(orgId, operatorId) {
75
+ const rows = await this.db
76
+ .select()
77
+ .from(posShifts)
78
+ .where(and(eq(posShifts.organizationId, orgId), eq(posShifts.operatorId, operatorId), eq(posShifts.status, "open")));
79
+ return Ok(rows[0] ?? null);
80
+ }
81
+ async getById(orgId, id) {
82
+ const rows = await this.db
83
+ .select()
84
+ .from(posShifts)
85
+ .where(and(eq(posShifts.id, id), eq(posShifts.organizationId, orgId)));
86
+ if (rows.length === 0)
87
+ return Err("Shift not found");
88
+ return Ok(rows[0]);
89
+ }
90
+ // ─── Cash Events ───────────────────────────────────────────────────
91
+ async addCashEvent(shiftId, input) {
92
+ if (input.amount <= 0)
93
+ return Err("Amount must be positive");
94
+ // Verify shift is open
95
+ const shifts = await this.db
96
+ .select()
97
+ .from(posShifts)
98
+ .where(eq(posShifts.id, shiftId));
99
+ if (shifts.length === 0)
100
+ return Err("Shift not found");
101
+ if (shifts[0].status !== "open")
102
+ return Err("Shift is not open");
103
+ const rows = await this.db
104
+ .insert(posCashEvents)
105
+ .values({
106
+ shiftId,
107
+ type: input.type,
108
+ amount: input.amount,
109
+ reason: input.reason,
110
+ performedBy: input.performedBy,
111
+ performedAt: new Date(),
112
+ })
113
+ .returning();
114
+ return Ok(rows[0]);
115
+ }
116
+ async listCashEvents(shiftId) {
117
+ const rows = await this.db
118
+ .select()
119
+ .from(posCashEvents)
120
+ .where(eq(posCashEvents.shiftId, shiftId))
121
+ .orderBy(desc(posCashEvents.performedAt));
122
+ return Ok(rows);
123
+ }
124
+ // ─── Z-Report ──────────────────────────────────────────────────────
125
+ async getReport(orgId, shiftId) {
126
+ const shiftResult = await this.getById(orgId, shiftId);
127
+ if (!shiftResult.ok)
128
+ return shiftResult;
129
+ const shift = shiftResult.value;
130
+ const cashEvents = await this.db
131
+ .select()
132
+ .from(posCashEvents)
133
+ .where(eq(posCashEvents.shiftId, shiftId))
134
+ .orderBy(desc(posCashEvents.performedAt));
135
+ // Payment method totals
136
+ const paymentRows = await this.db
137
+ .select({
138
+ method: posPayments.method,
139
+ total: sql `SUM(${posPayments.amount})`.as("total"),
140
+ count: sql `COUNT(*)`.as("count"),
141
+ })
142
+ .from(posPayments)
143
+ .innerJoin(posTransactions, eq(posPayments.transactionId, posTransactions.id))
144
+ .where(and(eq(posTransactions.shiftId, shiftId), eq(posTransactions.status, "completed")))
145
+ .groupBy(posPayments.method);
146
+ const transactionCountRows = await this.db
147
+ .select({ count: sql `COUNT(*)`.as("count") })
148
+ .from(posTransactions)
149
+ .where(and(eq(posTransactions.shiftId, shiftId), eq(posTransactions.status, "completed")));
150
+ return Ok({
151
+ shift,
152
+ cashEvents,
153
+ paymentMethodTotals: paymentRows.map((r) => ({
154
+ method: r.method,
155
+ total: Number(r.total),
156
+ count: Number(r.count),
157
+ })),
158
+ transactionCount: Number(transactionCountRows[0]?.count ?? 0),
159
+ });
160
+ }
161
+ // ─── Helpers ───────────────────────────────────────────────────────
162
+ async calculateExpectedCash(db, shiftId, openingFloat) {
163
+ // Cash payments collected during this shift
164
+ const cashPaymentRows = await db
165
+ .select({
166
+ total: sql `COALESCE(SUM(${posPayments.amount} - ${posPayments.changeGiven}), 0)`.as("total"),
167
+ })
168
+ .from(posPayments)
169
+ .innerJoin(posTransactions, eq(posPayments.transactionId, posTransactions.id))
170
+ .where(and(eq(posTransactions.shiftId, shiftId), eq(posPayments.method, "cash"), eq(posPayments.status, "collected")));
171
+ const cashFromSales = Number(cashPaymentRows[0]?.total ?? 0);
172
+ // Cash events: drops reduce, pickups add to drawer
173
+ const cashEventRows = await db
174
+ .select({
175
+ type: posCashEvents.type,
176
+ total: sql `SUM(${posCashEvents.amount})`.as("total"),
177
+ })
178
+ .from(posCashEvents)
179
+ .where(eq(posCashEvents.shiftId, shiftId))
180
+ .groupBy(posCashEvents.type);
181
+ let cashAdjustment = 0;
182
+ for (const row of cashEventRows) {
183
+ const amount = Number(row.total);
184
+ switch (row.type) {
185
+ case "drop":
186
+ case "paid_out":
187
+ cashAdjustment -= amount;
188
+ break;
189
+ case "pickup":
190
+ case "paid_in":
191
+ cashAdjustment += amount;
192
+ break;
193
+ // float is already accounted for in openingFloat
194
+ }
195
+ }
196
+ return openingFloat + cashFromSales + cashAdjustment;
197
+ }
198
+ }
@@ -0,0 +1,21 @@
1
+ import type { PluginResult } from "@unifiedcommerce/core";
2
+ import type { Db, Terminal } from "../types";
3
+ export declare class TerminalService {
4
+ private db;
5
+ constructor(db: Db);
6
+ create(orgId: string, input: {
7
+ name: string;
8
+ code: string;
9
+ type?: "register" | "tablet" | "mobile" | "kiosk";
10
+ metadata?: Record<string, unknown>;
11
+ }): Promise<PluginResult<Terminal>>;
12
+ list(orgId: string): Promise<PluginResult<Terminal[]>>;
13
+ getById(orgId: string, id: string): Promise<PluginResult<Terminal>>;
14
+ update(orgId: string, id: string, input: {
15
+ name?: string;
16
+ isActive?: boolean;
17
+ metadata?: Record<string, unknown>;
18
+ }): Promise<PluginResult<Terminal>>;
19
+ deactivate(orgId: string, id: string): Promise<PluginResult<Terminal>>;
20
+ }
21
+ //# sourceMappingURL=terminal-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-service.d.ts","sourceRoot":"","sources":["../../src/services/terminal-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAkB,MAAM,UAAU,CAAC;AAE7D,qBAAa,eAAe;IACd,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;QAClD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAyB7B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC;IAQtD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAUnE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;QAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAW7B,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;CAG7E"}
@@ -0,0 +1,59 @@
1
+ import { eq, and } from "drizzle-orm";
2
+ import { Ok, Err } from "@unifiedcommerce/core";
3
+ import { posTerminals } from "../schema";
4
+ export class TerminalService {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ async create(orgId, input) {
10
+ // Check for duplicate code in same org
11
+ const existing = await this.db
12
+ .select()
13
+ .from(posTerminals)
14
+ .where(and(eq(posTerminals.organizationId, orgId), eq(posTerminals.code, input.code)));
15
+ if (existing.length > 0) {
16
+ return Err(`Terminal with code '${input.code}' already exists`);
17
+ }
18
+ const rows = await this.db
19
+ .insert(posTerminals)
20
+ .values({
21
+ organizationId: orgId,
22
+ name: input.name,
23
+ code: input.code,
24
+ type: input.type ?? "register",
25
+ metadata: input.metadata ?? {},
26
+ })
27
+ .returning();
28
+ return Ok(rows[0]);
29
+ }
30
+ async list(orgId) {
31
+ const rows = await this.db
32
+ .select()
33
+ .from(posTerminals)
34
+ .where(eq(posTerminals.organizationId, orgId));
35
+ return Ok(rows);
36
+ }
37
+ async getById(orgId, id) {
38
+ const rows = await this.db
39
+ .select()
40
+ .from(posTerminals)
41
+ .where(and(eq(posTerminals.id, id), eq(posTerminals.organizationId, orgId)));
42
+ if (rows.length === 0)
43
+ return Err("Terminal not found");
44
+ return Ok(rows[0]);
45
+ }
46
+ async update(orgId, id, input) {
47
+ const rows = await this.db
48
+ .update(posTerminals)
49
+ .set({ ...input, updatedAt: new Date() })
50
+ .where(and(eq(posTerminals.id, id), eq(posTerminals.organizationId, orgId)))
51
+ .returning();
52
+ if (rows.length === 0)
53
+ return Err("Terminal not found");
54
+ return Ok(rows[0]);
55
+ }
56
+ async deactivate(orgId, id) {
57
+ return this.update(orgId, id, { isActive: false });
58
+ }
59
+ }
@@ -0,0 +1,30 @@
1
+ import type { PluginResult } from "@unifiedcommerce/core";
2
+ import type { Db, Transaction } from "../types";
3
+ export declare class TransactionService {
4
+ private db;
5
+ private transaction;
6
+ constructor(db: Db, transaction: (fn: (tx: Db) => Promise<unknown>) => Promise<unknown>);
7
+ create(orgId: string, input: {
8
+ shiftId: string;
9
+ terminalId: string;
10
+ operatorId: string;
11
+ cartId: string;
12
+ type?: "sale" | "return" | "exchange";
13
+ customerId?: string;
14
+ }): Promise<PluginResult<Transaction>>;
15
+ getById(orgId: string, id: string): Promise<PluginResult<Transaction>>;
16
+ hold(orgId: string, id: string, label: string): Promise<PluginResult<Transaction>>;
17
+ recall(orgId: string, id: string): Promise<PluginResult<Transaction>>;
18
+ listHeld(orgId: string, terminalId: string): Promise<PluginResult<Transaction[]>>;
19
+ void(orgId: string, id: string, reason: string): Promise<PluginResult<Transaction>>;
20
+ setCustomer(orgId: string, id: string, customerId: string): Promise<PluginResult<Transaction>>;
21
+ updateTotals(id: string, totals: {
22
+ subtotal: number;
23
+ taxTotal: number;
24
+ total: number;
25
+ discountTotal: number;
26
+ }): Promise<void>;
27
+ complete(id: string, orderId: string | null): Promise<PluginResult<Transaction>>;
28
+ private generateReceiptNumber;
29
+ }
30
+ //# sourceMappingURL=transaction-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-service.d.ts","sourceRoot":"","sources":["../../src/services/transaction-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,WAAW,EAAqB,MAAM,UAAU,CAAC;AAEnE,qBAAa,kBAAkB;IAE3B,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;gBADX,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC;IAGvE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;QACtC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IA4BhC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAUtE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAclF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAcrE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,CAAC;IAcjF,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAgCnF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAc9F,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;QACrC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;KACvB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOX,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;YAgExE,qBAAqB;CA0BpC"}
@@ -0,0 +1,202 @@
1
+ import { eq, and, desc, sql } from "drizzle-orm";
2
+ import { Ok, Err } from "@unifiedcommerce/core";
3
+ import { posTransactions, posPayments, posShifts } from "../schema";
4
+ export class TransactionService {
5
+ db;
6
+ transaction;
7
+ constructor(db, transaction) {
8
+ this.db = db;
9
+ this.transaction = transaction;
10
+ }
11
+ async create(orgId, input) {
12
+ // Verify shift is open
13
+ const shifts = await this.db
14
+ .select()
15
+ .from(posShifts)
16
+ .where(and(eq(posShifts.id, input.shiftId), eq(posShifts.status, "open")));
17
+ if (shifts.length === 0)
18
+ return Err("Shift is not open");
19
+ const receiptNumber = await this.generateReceiptNumber(input.terminalId);
20
+ const rows = await this.db
21
+ .insert(posTransactions)
22
+ .values({
23
+ organizationId: orgId,
24
+ shiftId: input.shiftId,
25
+ terminalId: input.terminalId,
26
+ operatorId: input.operatorId,
27
+ cartId: input.cartId,
28
+ type: input.type ?? "sale",
29
+ status: "open",
30
+ customerId: input.customerId,
31
+ receiptNumber,
32
+ })
33
+ .returning();
34
+ return Ok(rows[0]);
35
+ }
36
+ async getById(orgId, id) {
37
+ const rows = await this.db
38
+ .select()
39
+ .from(posTransactions)
40
+ .where(and(eq(posTransactions.id, id), eq(posTransactions.organizationId, orgId)));
41
+ if (rows.length === 0)
42
+ return Err("Transaction not found");
43
+ return Ok(rows[0]);
44
+ }
45
+ async hold(orgId, id, label) {
46
+ const txnResult = await this.getById(orgId, id);
47
+ if (!txnResult.ok)
48
+ return txnResult;
49
+ if (txnResult.value.status !== "open")
50
+ return Err("Only open transactions can be held");
51
+ const rows = await this.db
52
+ .update(posTransactions)
53
+ .set({ status: "held", holdLabel: label, updatedAt: new Date() })
54
+ .where(eq(posTransactions.id, id))
55
+ .returning();
56
+ return Ok(rows[0]);
57
+ }
58
+ async recall(orgId, id) {
59
+ const txnResult = await this.getById(orgId, id);
60
+ if (!txnResult.ok)
61
+ return txnResult;
62
+ if (txnResult.value.status !== "held")
63
+ return Err("Only held transactions can be recalled");
64
+ const rows = await this.db
65
+ .update(posTransactions)
66
+ .set({ status: "open", holdLabel: null, updatedAt: new Date() })
67
+ .where(eq(posTransactions.id, id))
68
+ .returning();
69
+ return Ok(rows[0]);
70
+ }
71
+ async listHeld(orgId, terminalId) {
72
+ const rows = await this.db
73
+ .select()
74
+ .from(posTransactions)
75
+ .where(and(eq(posTransactions.organizationId, orgId), eq(posTransactions.terminalId, terminalId), eq(posTransactions.status, "held")))
76
+ .orderBy(desc(posTransactions.createdAt));
77
+ return Ok(rows);
78
+ }
79
+ async void(orgId, id, reason) {
80
+ const result = await this.transaction(async (tx) => {
81
+ const txns = await tx
82
+ .select()
83
+ .from(posTransactions)
84
+ .where(and(eq(posTransactions.id, id), eq(posTransactions.organizationId, orgId)))
85
+ .for("update");
86
+ if (txns.length === 0)
87
+ return Err("Transaction not found");
88
+ const txn = txns[0];
89
+ if (txn.status === "completed")
90
+ return Err("Cannot void a completed transaction");
91
+ if (txn.status === "voided")
92
+ return Err("Transaction is already voided");
93
+ const updated = await tx
94
+ .update(posTransactions)
95
+ .set({ status: "voided", voidReason: reason, updatedAt: new Date() })
96
+ .where(eq(posTransactions.id, id))
97
+ .returning();
98
+ // Increment void count on shift
99
+ await tx
100
+ .update(posShifts)
101
+ .set({ voidsCount: sql `${posShifts.voidsCount} + 1`, updatedAt: new Date() })
102
+ .where(eq(posShifts.id, txn.shiftId));
103
+ return Ok(updated[0]);
104
+ });
105
+ return result;
106
+ }
107
+ async setCustomer(orgId, id, customerId) {
108
+ const txnResult = await this.getById(orgId, id);
109
+ if (!txnResult.ok)
110
+ return txnResult;
111
+ if (txnResult.value.status !== "open")
112
+ return Err("Can only set customer on open transactions");
113
+ const rows = await this.db
114
+ .update(posTransactions)
115
+ .set({ customerId, updatedAt: new Date() })
116
+ .where(eq(posTransactions.id, id))
117
+ .returning();
118
+ return Ok(rows[0]);
119
+ }
120
+ async updateTotals(id, totals) {
121
+ await this.db
122
+ .update(posTransactions)
123
+ .set({ ...totals, updatedAt: new Date() })
124
+ .where(eq(posTransactions.id, id));
125
+ }
126
+ async complete(id, orderId) {
127
+ const result = await this.transaction(async (tx) => {
128
+ // Lock and verify transaction is in a completable state
129
+ const existing = await tx
130
+ .select()
131
+ .from(posTransactions)
132
+ .where(eq(posTransactions.id, id))
133
+ .for("update");
134
+ if (existing.length === 0)
135
+ return Err("Transaction not found");
136
+ const current = existing[0];
137
+ if (current.status === "completed")
138
+ return Err("Transaction is already completed");
139
+ if (current.status === "voided")
140
+ return Err("Cannot complete a voided transaction");
141
+ if (current.status !== "open")
142
+ return Err(`Cannot complete transaction in '${current.status}' status`);
143
+ // Sum collected payments to derive transaction total
144
+ const paymentRows = await tx
145
+ .select({
146
+ total: sql `COALESCE(SUM(${posPayments.amount} - ${posPayments.changeGiven}), 0)`.as("total"),
147
+ })
148
+ .from(posPayments)
149
+ .where(and(eq(posPayments.transactionId, id), eq(posPayments.status, "collected")));
150
+ const paymentTotal = Number(paymentRows[0]?.total ?? 0);
151
+ const rows = await tx
152
+ .update(posTransactions)
153
+ .set({
154
+ status: "completed",
155
+ subtotal: paymentTotal,
156
+ total: paymentTotal,
157
+ ...(orderId != null ? { orderId } : {}),
158
+ completedAt: new Date(),
159
+ updatedAt: new Date(),
160
+ })
161
+ .where(eq(posTransactions.id, id))
162
+ .returning();
163
+ if (rows.length === 0)
164
+ return Err("Transaction not found");
165
+ const txn = rows[0];
166
+ // Update shift sales counters
167
+ if (txn.type === "sale" && paymentTotal > 0) {
168
+ await tx
169
+ .update(posShifts)
170
+ .set({
171
+ salesCount: sql `${posShifts.salesCount} + 1`,
172
+ salesTotal: sql `${posShifts.salesTotal} + ${paymentTotal}`,
173
+ updatedAt: new Date(),
174
+ })
175
+ .where(eq(posShifts.id, txn.shiftId));
176
+ }
177
+ return Ok(txn);
178
+ });
179
+ return result;
180
+ }
181
+ // ─── Receipt Number Generation ─────────────────────────────────────
182
+ // Sequential per terminal per day: {terminal_code}-{sequence}
183
+ async generateReceiptNumber(terminalId) {
184
+ const today = new Date();
185
+ const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
186
+ const startOfDayISO = startOfDay.toISOString();
187
+ // Get terminal code
188
+ const { posTerminals } = await import("../schema");
189
+ const terminals = await this.db
190
+ .select({ code: posTerminals.code })
191
+ .from(posTerminals)
192
+ .where(eq(posTerminals.id, terminalId));
193
+ const terminalCode = terminals[0]?.code ?? "POS";
194
+ // Count today's transactions for this terminal
195
+ const countRows = await this.db
196
+ .select({ count: sql `COUNT(*)`.as("count") })
197
+ .from(posTransactions)
198
+ .where(and(eq(posTransactions.terminalId, terminalId), sql `${posTransactions.createdAt} >= ${startOfDayISO}::timestamptz`));
199
+ const seq = Number(countRows[0]?.count ?? 0) + 1;
200
+ return `${terminalCode}-${String(seq).padStart(4, "0")}`;
201
+ }
202
+ }
@@ -0,0 +1,30 @@
1
+ export type { PluginDb as Db } from "@unifiedcommerce/core";
2
+ import type { posTerminals, posShifts, posCashEvents, posTransactions, posPayments, posReturnItems } from "./schema";
3
+ export type Terminal = typeof posTerminals.$inferSelect;
4
+ export type TerminalInsert = typeof posTerminals.$inferInsert;
5
+ export type Shift = typeof posShifts.$inferSelect;
6
+ export type ShiftInsert = typeof posShifts.$inferInsert;
7
+ export type CashEvent = typeof posCashEvents.$inferSelect;
8
+ export type CashEventInsert = typeof posCashEvents.$inferInsert;
9
+ export type Transaction = typeof posTransactions.$inferSelect;
10
+ export type TransactionInsert = typeof posTransactions.$inferInsert;
11
+ export type Payment = typeof posPayments.$inferSelect;
12
+ export type PaymentInsert = typeof posPayments.$inferInsert;
13
+ export type ReturnItem = typeof posReturnItems.$inferSelect;
14
+ export type ReturnItemInsert = typeof posReturnItems.$inferInsert;
15
+ export type TransactionStatus = "open" | "held" | "completed" | "voided";
16
+ export type TransactionType = "sale" | "return" | "exchange";
17
+ export type PaymentMethod = "cash" | "card" | "gift_card" | "store_credit" | "other";
18
+ export type CashEventType = "float" | "drop" | "pickup" | "paid_in" | "paid_out";
19
+ export type ShiftStatus = "open" | "closed";
20
+ export type TerminalType = "register" | "tablet" | "mobile" | "kiosk";
21
+ export interface POSPluginOptions {
22
+ /** Default currency for new transactions. Default: "USD" */
23
+ defaultCurrency?: string;
24
+ /** Maximum hold duration in hours before auto-void. Default: 24 */
25
+ maxHoldHours?: number;
26
+ /** Require manager override for discounts above this percentage. Default: 20 */
27
+ discountOverrideThreshold?: number;
28
+ }
29
+ export declare const DEFAULT_POS_OPTIONS: Required<POSPluginOptions>;
30
+ //# 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,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,aAAa,EACb,eAAe,EACf,WAAW,EACX,cAAc,EACf,MAAM,UAAU,CAAC;AAElB,MAAM,MAAM,QAAQ,GAAG,OAAO,YAAY,CAAC,YAAY,CAAC;AACxD,MAAM,MAAM,cAAc,GAAG,OAAO,YAAY,CAAC,YAAY,CAAC;AAE9D,MAAM,MAAM,KAAK,GAAG,OAAO,SAAS,CAAC,YAAY,CAAC;AAClD,MAAM,MAAM,WAAW,GAAG,OAAO,SAAS,CAAC,YAAY,CAAC;AAExD,MAAM,MAAM,SAAS,GAAG,OAAO,aAAa,CAAC,YAAY,CAAC;AAC1D,MAAM,MAAM,eAAe,GAAG,OAAO,aAAa,CAAC,YAAY,CAAC;AAEhE,MAAM,MAAM,WAAW,GAAG,OAAO,eAAe,CAAC,YAAY,CAAC;AAC9D,MAAM,MAAM,iBAAiB,GAAG,OAAO,eAAe,CAAC,YAAY,CAAC;AAEpE,MAAM,MAAM,OAAO,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC;AACtD,MAAM,MAAM,aAAa,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC;AAE5D,MAAM,MAAM,UAAU,GAAG,OAAO,cAAc,CAAC,YAAY,CAAC;AAC5D,MAAM,MAAM,gBAAgB,GAAG,OAAO,cAAc,CAAC,YAAY,CAAC;AAElE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AACzE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;AAC7D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAC;AACrF,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;AACjF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAC;AAC5C,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,gBAAgB,CAI1D,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_POS_OPTIONS = {
2
+ defaultCurrency: "USD",
3
+ maxHoldHours: 24,
4
+ discountOverrideThreshold: 20,
5
+ };