@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
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@unifiedcommerce/plugin-pos",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.build.json",
13
+ "check-types": "tsc --noEmit",
14
+ "lint": "eslint . --max-warnings 1000",
15
+ "test": "vitest run"
16
+ },
17
+ "dependencies": {
18
+ "@hono/zod-openapi": "^1.2.2",
19
+ "@unifiedcommerce/core": "*",
20
+ "@unifiedcommerce/db": "*",
21
+ "drizzle-orm": "^0.45.1",
22
+ "hono": "^4.12.5"
23
+ },
24
+ "devDependencies": {
25
+ "@repo/eslint-config": "*",
26
+ "@repo/typescript-config": "*",
27
+ "@types/node": "^24.5.2",
28
+ "eslint": "^9.39.1",
29
+ "typescript": "5.9.2",
30
+ "vitest": "^3.2.4"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "src",
38
+ "README.md"
39
+ ]
40
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * POS-specific checkout hooks.
3
+ *
4
+ * checkout.beforePayment: Sets shippingTotal = 0 for POS transactions (no shipping).
5
+ * checkout.afterCreate: Updates POS transaction with orderId, increments shift counters.
6
+ */
7
+
8
+ import { eq, sql } from "drizzle-orm";
9
+ import { posTransactions, posShifts } from "../schema";
10
+ import type { Db } from "../types";
11
+
12
+ interface CheckoutData {
13
+ metadata?: Record<string, unknown> | null;
14
+ shippingTotal: number;
15
+ shippingAddress?: unknown;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ interface OrderResult {
20
+ id: string;
21
+ grandTotal?: number;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ interface HookContext {
26
+ tx?: unknown;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ /**
31
+ * Before payment hook: zero out shipping for POS transactions.
32
+ */
33
+ export function buildPOSShippingHook() {
34
+ return {
35
+ key: "checkout.beforePayment",
36
+ handler: async (...args: unknown[]) => {
37
+ const hook = args[0] as { data: CheckoutData; context: HookContext };
38
+ const posTransactionId = hook.data.metadata?.posTransactionId;
39
+ if (!posTransactionId) return hook.data;
40
+
41
+ // POS transactions have no shipping
42
+ hook.data.shippingTotal = 0;
43
+ hook.data.shippingAddress = undefined;
44
+ return hook.data;
45
+ },
46
+ };
47
+ }
48
+
49
+ /**
50
+ * After create hook: finalize POS transaction with order details.
51
+ */
52
+ export function buildPOSFinalizationHook(getDb: () => Db) {
53
+ return {
54
+ key: "checkout.afterCreate",
55
+ handler: async (...args: unknown[]) => {
56
+ const hook = args[0] as { result: OrderResult; context: HookContext };
57
+ const result = hook.result;
58
+ if (!result) return;
59
+
60
+ // Check if this checkout was initiated by a POS transaction
61
+ // The metadata is on the order, not directly on the hook result
62
+ const metadata = (result as Record<string, unknown>).metadata as Record<string, unknown> | undefined;
63
+ const posTransactionId = metadata?.posTransactionId as string | undefined;
64
+ if (!posTransactionId) return;
65
+
66
+ const db = getDb();
67
+
68
+ // Update transaction with orderId and status
69
+ await db
70
+ .update(posTransactions)
71
+ .set({
72
+ orderId: result.id,
73
+ status: "completed",
74
+ completedAt: new Date(),
75
+ updatedAt: new Date(),
76
+ })
77
+ .where(eq(posTransactions.id, posTransactionId));
78
+
79
+ // Increment shift sales counters
80
+ const posShiftId = metadata?.posShiftId as string | undefined;
81
+ if (posShiftId) {
82
+ await db
83
+ .update(posShifts)
84
+ .set({
85
+ salesCount: sql`${posShifts.salesCount} + 1`,
86
+ salesTotal: sql`${posShifts.salesTotal} + ${result.grandTotal ?? 0}`,
87
+ updatedAt: new Date(),
88
+ })
89
+ .where(eq(posShifts.id, posShiftId));
90
+ }
91
+ },
92
+ };
93
+ }
package/src/index.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { defineCommercePlugin } from "@unifiedcommerce/core";
2
+ import {
3
+ posTerminals,
4
+ posShifts,
5
+ posCashEvents,
6
+ posTransactions,
7
+ posPayments,
8
+ posReturnItems,
9
+ } from "./schema";
10
+ import { TerminalService } from "./services/terminal-service";
11
+ import { ShiftService } from "./services/shift-service";
12
+ import { TransactionService } from "./services/transaction-service";
13
+ import { PaymentService } from "./services/payment-service";
14
+ import { ReturnService } from "./services/return-service";
15
+ import { LookupService } from "./services/lookup-service";
16
+ import { ReceiptService } from "./services/receipt-service";
17
+ import { buildTerminalRoutes } from "./routes/terminals";
18
+ import { buildShiftRoutes } from "./routes/shifts";
19
+ import { buildTransactionRoutes } from "./routes/transactions";
20
+ import { buildPaymentRoutes } from "./routes/payments";
21
+ import { buildReturnRoutes } from "./routes/returns";
22
+ import { buildLookupRoutes } from "./routes/lookup";
23
+ import { buildReceiptRoutes } from "./routes/receipts";
24
+ import { buildPOSShippingHook, buildPOSFinalizationHook } from "./hooks/checkout-pos";
25
+ import type { POSPluginOptions, Db } from "./types";
26
+ import { DEFAULT_POS_OPTIONS } from "./types";
27
+
28
+ export type { POSPluginOptions } from "./types";
29
+ export { TerminalService } from "./services/terminal-service";
30
+ export { ShiftService } from "./services/shift-service";
31
+ export { TransactionService } from "./services/transaction-service";
32
+ export { PaymentService } from "./services/payment-service";
33
+ export { ReturnService } from "./services/return-service";
34
+ export { LookupService } from "./services/lookup-service";
35
+ export { ReceiptService } from "./services/receipt-service";
36
+ export { createPOSPaymentAdapter } from "./payment-adapter";
37
+
38
+ /**
39
+ * POS Plugin — Tier 0 Core Primitives (RFC-023)
40
+ *
41
+ * Provides:
42
+ * - Terminal management (register, tablet, mobile, kiosk)
43
+ * - Shift management (open/close with cash tracking, Z-report)
44
+ * - Transaction lifecycle (create, hold/recall, void, complete)
45
+ * - Split payment support (cash, card, gift card, store credit)
46
+ * - Returns with original order linkage
47
+ * - Barcode/SKU lookup via indexed queries
48
+ * - Receipt data assembly + email
49
+ * - POS payment adapter for checkout pipeline
50
+ * - Checkout hooks (zero shipping, transaction finalization)
51
+ */
52
+ export function posPlugin(userOptions: POSPluginOptions = {}) {
53
+ const options: Required<POSPluginOptions> = {
54
+ ...DEFAULT_POS_OPTIONS,
55
+ ...userOptions,
56
+ };
57
+
58
+ // Shared DB reference for hooks (populated in routes())
59
+ const dbRef: { current: Db | null } = { current: null };
60
+
61
+ return defineCommercePlugin({
62
+ id: "pos",
63
+ version: "1.0.0",
64
+
65
+ permissions: [
66
+ {
67
+ scope: "pos:admin",
68
+ description: "Register terminals, view all shifts, Z-reports, configure POS settings.",
69
+ },
70
+ {
71
+ scope: "pos:manage",
72
+ description: "Void transactions, apply discounts, process returns, override price.",
73
+ },
74
+ {
75
+ scope: "pos:operate",
76
+ description: "Open/close shifts, ring up sales, accept payment, hold/recall, reprint receipts.",
77
+ },
78
+ ],
79
+
80
+ schema: () => ({
81
+ posTerminals,
82
+ posShifts,
83
+ posCashEvents,
84
+ posTransactions,
85
+ posPayments,
86
+ posReturnItems,
87
+ }),
88
+
89
+ hooks: () => [
90
+ buildPOSShippingHook(),
91
+ buildPOSFinalizationHook(() => {
92
+ if (!dbRef.current) throw new Error("POS plugin DB not initialized — hooks ran before routes()");
93
+ return dbRef.current;
94
+ }),
95
+ ],
96
+
97
+ routes: (ctx) => {
98
+ const db = ctx.database.db as unknown as Db;
99
+ if (!db) return [];
100
+
101
+ // Populate the shared DB reference for hooks
102
+ dbRef.current = db;
103
+
104
+ const transactionFn = ctx.database.transaction as unknown as (fn: (tx: Db) => Promise<unknown>) => Promise<unknown>;
105
+
106
+ // Initialize services
107
+ const terminalService = new TerminalService(db);
108
+ const shiftService = new ShiftService(db, transactionFn);
109
+ const transactionService = new TransactionService(db, transactionFn);
110
+ const paymentService = new PaymentService(db, transactionFn);
111
+ const returnService = new ReturnService(db);
112
+ const lookupService = new LookupService(db, ctx.services);
113
+ const receiptService = new ReceiptService(db, ctx.services);
114
+
115
+ // Cart service from core (for creating POS transaction carts)
116
+ const cartService = ctx.services.cart as {
117
+ create: (input: { currency?: string; metadata?: Record<string, unknown> }, actor: unknown) => Promise<{ ok: boolean; value?: { id: string } }>;
118
+ };
119
+
120
+ return [
121
+ ...buildTerminalRoutes(terminalService, ctx),
122
+ ...buildShiftRoutes(shiftService, ctx),
123
+ ...buildTransactionRoutes(transactionService, cartService, ctx),
124
+ ...buildPaymentRoutes(paymentService, transactionService, ctx),
125
+ ...buildReturnRoutes(returnService, transactionService, paymentService, cartService, ctx),
126
+ ...buildLookupRoutes(lookupService, ctx),
127
+ ...buildReceiptRoutes(receiptService, ctx),
128
+ ];
129
+ },
130
+ });
131
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * POS Payment Adapter for the checkout pipeline.
3
+ *
4
+ * POS payments are collected at the terminal before checkout is called.
5
+ * The adapter acts as a pass-through: payment has already been tendered,
6
+ * so authorize/capture are no-ops.
7
+ */
8
+
9
+ export interface PaymentAdapter {
10
+ providerId: string;
11
+ createPaymentIntent(params: {
12
+ amount: number;
13
+ currency: string;
14
+ orderId?: string;
15
+ metadata?: Record<string, unknown>;
16
+ }): Promise<{ id: string; status: string; amount: number; clientSecret?: string }>;
17
+ capturePayment(intentId: string, amount: number): Promise<{ id: string; status: string; amountCaptured: number }>;
18
+ refundPayment(paymentId: string, amount: number): Promise<{ id: string; status: string; amountRefunded: number }>;
19
+ }
20
+
21
+ export function createPOSPaymentAdapter(): PaymentAdapter {
22
+ return {
23
+ providerId: "pos",
24
+
25
+ async createPaymentIntent(params) {
26
+ // POS payments are already collected at the terminal.
27
+ // The "intent" represents the sum of pos_payments rows.
28
+ return {
29
+ id: `pos_${params.orderId ?? crypto.randomUUID()}`,
30
+ status: "requires_capture",
31
+ amount: params.amount,
32
+ };
33
+ },
34
+
35
+ async capturePayment(intentId, amount) {
36
+ // POS payments are already collected. Capture is a no-op.
37
+ return {
38
+ id: intentId,
39
+ status: "succeeded",
40
+ amountCaptured: amount,
41
+ };
42
+ },
43
+
44
+ async refundPayment(paymentId, amount) {
45
+ // POS refunds are handled by the return transaction flow.
46
+ return {
47
+ id: `ref_${paymentId}`,
48
+ status: "succeeded",
49
+ amountRefunded: amount,
50
+ };
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,44 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import type { LookupService } from "../services/lookup-service";
4
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
5
+
6
+ export function buildLookupRoutes(
7
+ service: LookupService,
8
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
9
+ ): PluginRouteRegistration[] {
10
+ const r = router("POS Lookup", "/pos/lookup", ctx);
11
+
12
+ r.get("/barcode/{code}")
13
+ .summary("Lookup by barcode")
14
+ .permission("pos:operate")
15
+ .handler(async ({ params, orgId }) => {
16
+ const result = await service.byBarcode(orgId, params.code!);
17
+ if (!result.ok) throw new Error(result.error);
18
+ return result.value;
19
+ });
20
+
21
+ r.get("/sku/{sku}")
22
+ .summary("Lookup by SKU")
23
+ .permission("pos:operate")
24
+ .handler(async ({ params, orgId }) => {
25
+ const result = await service.bySku(orgId, params.sku!);
26
+ if (!result.ok) throw new Error(result.error);
27
+ return result.value;
28
+ });
29
+
30
+ r.get("/search")
31
+ .summary("Search items")
32
+ .permission("pos:operate")
33
+ .query(z.object({
34
+ q: z.string().min(1).max(200),
35
+ }))
36
+ .handler(async ({ query, orgId }) => {
37
+ const q = query as { q: string };
38
+ const result = await service.search(orgId, q.q);
39
+ if (!result.ok) throw new Error(result.error);
40
+ return result.value;
41
+ });
42
+
43
+ return r.routes();
44
+ }
@@ -0,0 +1,82 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import type { PaymentService } from "../services/payment-service";
4
+ import type { TransactionService } from "../services/transaction-service";
5
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
6
+
7
+ export function buildPaymentRoutes(
8
+ paymentService: PaymentService,
9
+ transactionService: TransactionService,
10
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
11
+ ): PluginRouteRegistration[] {
12
+ const r = router("POS Payments", "/pos/transactions", ctx);
13
+
14
+ r.post("/{id}/payments")
15
+ .summary("Add payment")
16
+ .permission("pos:operate")
17
+ .input(z.object({
18
+ method: z.enum(["cash", "card", "gift_card", "store_credit", "other"]),
19
+ amount: z.number().int().positive(),
20
+ changeGiven: z.number().int().min(0).optional(),
21
+ reference: z.string().max(200).optional(),
22
+ metadata: z.record(z.string(), z.unknown()).optional(),
23
+ }))
24
+ .handler(async ({ params, input, orgId }) => {
25
+ const body = input as {
26
+ method: "cash" | "card" | "gift_card" | "store_credit" | "other";
27
+ amount: number;
28
+ changeGiven?: number;
29
+ reference?: string;
30
+ metadata?: Record<string, unknown>;
31
+ };
32
+ const result = await paymentService.addPayment(orgId, params.id!, body);
33
+ if (!result.ok) throw new Error(result.error);
34
+ return result.value;
35
+ });
36
+
37
+ r.post("/{id}/complete")
38
+ .summary("Complete transaction")
39
+ .description("Validates total payments >= total, then calls checkout pipeline.")
40
+ .permission("pos:operate")
41
+ .handler(async ({ params, services, orgId }) => {
42
+ // Validate payments cover the total
43
+ const validation = await paymentService.validateForCompletion(orgId, params.id!);
44
+ if (!validation.ok) throw new Error(validation.error);
45
+
46
+ const { transaction } = validation.value;
47
+
48
+ // Call checkout pipeline via the checkout service
49
+ // The POS payment adapter handles the payment side
50
+ const checkout = services as Record<string, unknown>;
51
+
52
+ // Build checkout request — POS goes through the same pipeline as online
53
+ const checkoutBody = {
54
+ cartId: transaction.cartId,
55
+ currency: "USD",
56
+ paymentMethodId: "pos",
57
+ metadata: {
58
+ posTransactionId: transaction.id,
59
+ posShiftId: transaction.shiftId,
60
+ posTerminalId: transaction.terminalId,
61
+ },
62
+ ...(transaction.customerId != null ? { customerId: transaction.customerId } : {}),
63
+ };
64
+
65
+ // We need to make an internal call to the checkout pipeline
66
+ // This is handled by POSCheckoutAdapter in hooks/checkout-pos.ts
67
+ // For now, return the transaction with payment status
68
+ // The plugin entry point wires the checkout hook
69
+
70
+ // Mark transaction as completed (the afterCreate hook will set orderId)
71
+ const result = await transactionService.complete(params.id!, null);
72
+ if (!result.ok) throw new Error(result.error);
73
+
74
+ return {
75
+ transaction: result.value,
76
+ checkoutRequest: checkoutBody,
77
+ message: "Transaction completed. Use POST /api/checkout with the checkoutRequest body to finalize.",
78
+ };
79
+ });
80
+
81
+ return r.routes();
82
+ }
@@ -0,0 +1,35 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import type { ReceiptService } from "../services/receipt-service";
4
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
5
+
6
+ export function buildReceiptRoutes(
7
+ service: ReceiptService,
8
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
9
+ ): PluginRouteRegistration[] {
10
+ const r = router("POS Receipts", "/pos/transactions", ctx);
11
+
12
+ r.get("/{id}/receipt")
13
+ .summary("Get receipt")
14
+ .permission("pos:operate")
15
+ .handler(async ({ params, orgId }) => {
16
+ const result = await service.getReceipt(orgId, params.id!);
17
+ if (!result.ok) throw new Error(result.error);
18
+ return result.value;
19
+ });
20
+
21
+ r.post("/{id}/receipt/email")
22
+ .summary("Email receipt")
23
+ .permission("pos:operate")
24
+ .input(z.object({
25
+ email: z.string().email(),
26
+ }))
27
+ .handler(async ({ params, input, orgId }) => {
28
+ const body = input as { email: string };
29
+ const result = await service.emailReceipt(orgId, params.id!, body.email);
30
+ if (!result.ok) throw new Error(result.error);
31
+ return result.value;
32
+ });
33
+
34
+ return r.routes();
35
+ }
@@ -0,0 +1,116 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import type { ReturnService } from "../services/return-service";
4
+ import type { TransactionService } from "../services/transaction-service";
5
+ import type { PaymentService } from "../services/payment-service";
6
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
7
+
8
+ export function buildReturnRoutes(
9
+ returnService: ReturnService,
10
+ transactionService: TransactionService,
11
+ paymentService: PaymentService,
12
+ cartService: { create: (input: { currency?: string; metadata?: Record<string, unknown> }, actor: unknown) => Promise<{ ok: boolean; value?: { id: string } }> },
13
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
14
+ ): PluginRouteRegistration[] {
15
+ const r = router("POS Returns", "/pos/returns", ctx);
16
+
17
+ r.post("/")
18
+ .summary("Create return")
19
+ .permission("pos:manage")
20
+ .input(z.object({
21
+ shiftId: z.string().uuid(),
22
+ terminalId: z.string().uuid(),
23
+ originalOrderId: z.string().uuid(),
24
+ items: z.array(z.object({
25
+ originalLineItemId: z.string().uuid(),
26
+ quantity: z.number().int().positive(),
27
+ reason: z.enum(["defective", "wrong_item", "changed_mind", "other"]),
28
+ restockingFee: z.number().int().min(0).optional(),
29
+ refundAmount: z.number().int().positive(),
30
+ })).min(1),
31
+ }))
32
+ .handler(async ({ input, actor, orgId }) => {
33
+ const body = input as {
34
+ shiftId: string;
35
+ terminalId: string;
36
+ originalOrderId: string;
37
+ items: Array<{
38
+ originalLineItemId: string;
39
+ quantity: number;
40
+ reason: "defective" | "wrong_item" | "changed_mind" | "other";
41
+ restockingFee?: number;
42
+ refundAmount: number;
43
+ }>;
44
+ };
45
+
46
+ // Create a cart for the return transaction
47
+ const cartResult = await cartService.create(
48
+ { currency: "USD", metadata: { posReturn: true } },
49
+ actor,
50
+ );
51
+ if (!cartResult.ok || !cartResult.value) {
52
+ throw new Error("Failed to create cart for return");
53
+ }
54
+
55
+ // Create return transaction
56
+ const txnResult = await transactionService.create(orgId, {
57
+ shiftId: body.shiftId,
58
+ terminalId: body.terminalId,
59
+ operatorId: actor!.userId,
60
+ cartId: cartResult.value.id,
61
+ type: "return",
62
+ });
63
+ if (!txnResult.ok) throw new Error(txnResult.error);
64
+
65
+ // Record return items
66
+ const itemsResult = await returnService.addReturnItems(
67
+ txnResult.value.id,
68
+ body.items.map((item) => ({
69
+ ...item,
70
+ originalOrderId: body.originalOrderId,
71
+ })),
72
+ );
73
+ if (!itemsResult.ok) throw new Error(itemsResult.error);
74
+
75
+ // Update transaction totals
76
+ const refundTotal = body.items.reduce((sum, i) => sum + i.refundAmount, 0);
77
+ await transactionService.updateTotals(txnResult.value.id, {
78
+ subtotal: refundTotal,
79
+ taxTotal: 0,
80
+ total: refundTotal,
81
+ discountTotal: 0,
82
+ });
83
+
84
+ return {
85
+ transaction: txnResult.value,
86
+ returnItems: itemsResult.value,
87
+ refundTotal,
88
+ };
89
+ });
90
+
91
+ r.post("/{id}/payments")
92
+ .summary("Add refund payment")
93
+ .permission("pos:operate")
94
+ .input(z.object({
95
+ method: z.enum(["cash", "card", "gift_card", "store_credit", "other"]),
96
+ amount: z.number().int().positive(),
97
+ reference: z.string().max(200).optional(),
98
+ }))
99
+ .handler(async ({ params, input, orgId }) => {
100
+ const body = input as { method: "cash" | "card" | "gift_card" | "store_credit" | "other"; amount: number; reference?: string };
101
+ const result = await paymentService.addPayment(orgId, params.id!, body);
102
+ if (!result.ok) throw new Error(result.error);
103
+ return result.value;
104
+ });
105
+
106
+ r.post("/{id}/complete")
107
+ .summary("Complete return")
108
+ .permission("pos:operate")
109
+ .handler(async ({ params, actor }) => {
110
+ const result = await transactionService.complete(params.id!, null);
111
+ if (!result.ok) throw new Error(result.error);
112
+ return result.value;
113
+ });
114
+
115
+ return r.routes();
116
+ }