@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 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAatB,CAAC;AAIJ,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsBnB,CAAC;AAIJ,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWvB,CAAC;AAIJ,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BzB,CAAC;AAIJ,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAarB,CAAC;AAIJ,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAaxB,CAAC"}
package/dist/schema.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * POS Plugin Schema — RFC-023 Tier 0 Core Primitives
3
+ *
4
+ * 6 tables replacing the old single pos_sessions table:
5
+ * - pos_terminals: physical register/device registry
6
+ * - pos_shifts: operator working periods with cash tracking
7
+ * - pos_cash_events: cash drawer operations within a shift
8
+ * - pos_transactions: individual sales, returns, exchanges
9
+ * - pos_payments: payment records (supports split tender)
10
+ * - pos_return_items: links return transactions to original order line items
11
+ */
12
+ import { pgTable, uuid, text, integer, boolean, timestamp, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
13
+ // ─── Terminals ──────────────────────────────────────────────────────────
14
+ export const posTerminals = pgTable("pos_terminals", {
15
+ id: uuid("id").defaultRandom().primaryKey(),
16
+ organizationId: text("organization_id").notNull(),
17
+ name: text("name").notNull(),
18
+ code: text("code").notNull(),
19
+ type: text("type", { enum: ["register", "tablet", "mobile", "kiosk"] }).notNull().default("register"),
20
+ isActive: boolean("is_active").notNull().default(true),
21
+ metadata: jsonb("metadata").$type().default({}),
22
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
23
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
24
+ }, (table) => ({
25
+ orgIdx: index("idx_pos_terminals_org").on(table.organizationId),
26
+ codeUnique: uniqueIndex("pos_terminals_org_code_unique").on(table.organizationId, table.code),
27
+ }));
28
+ // ─── Shifts ─────────────────────────────────────────────────────────────
29
+ export const posShifts = pgTable("pos_shifts", {
30
+ id: uuid("id").defaultRandom().primaryKey(),
31
+ organizationId: text("organization_id").notNull(),
32
+ terminalId: uuid("terminal_id").references(() => posTerminals.id, { onDelete: "cascade" }).notNull(),
33
+ operatorId: text("operator_id").notNull(),
34
+ status: text("status", { enum: ["open", "closed"] }).notNull().default("open"),
35
+ openingFloat: integer("opening_float").notNull().default(0),
36
+ closingCount: integer("closing_count"),
37
+ expectedCash: integer("expected_cash"),
38
+ cashVariance: integer("cash_variance"),
39
+ salesCount: integer("sales_count").notNull().default(0),
40
+ salesTotal: integer("sales_total").notNull().default(0),
41
+ refundsCount: integer("refunds_count").notNull().default(0),
42
+ refundsTotal: integer("refunds_total").notNull().default(0),
43
+ voidsCount: integer("voids_count").notNull().default(0),
44
+ closedAt: timestamp("closed_at", { withTimezone: true }),
45
+ metadata: jsonb("metadata").$type().default({}),
46
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
47
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
48
+ }, (table) => ({
49
+ orgIdx: index("idx_pos_shifts_org").on(table.organizationId),
50
+ terminalIdx: index("idx_pos_shifts_terminal").on(table.terminalId),
51
+ }));
52
+ // ─── Cash Events ────────────────────────────────────────────────────────
53
+ export const posCashEvents = pgTable("pos_cash_events", {
54
+ id: uuid("id").defaultRandom().primaryKey(),
55
+ shiftId: uuid("shift_id").references(() => posShifts.id, { onDelete: "cascade" }).notNull(),
56
+ type: text("type", { enum: ["float", "drop", "pickup", "paid_in", "paid_out"] }).notNull(),
57
+ amount: integer("amount").notNull(),
58
+ reason: text("reason"),
59
+ performedBy: text("performed_by").notNull(),
60
+ performedAt: timestamp("performed_at", { withTimezone: true }).defaultNow().notNull(),
61
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
62
+ }, (table) => ({
63
+ shiftIdx: index("idx_pos_cash_events_shift").on(table.shiftId),
64
+ }));
65
+ // ─── Transactions ───────────────────────────────────────────────────────
66
+ export const posTransactions = pgTable("pos_transactions", {
67
+ id: uuid("id").defaultRandom().primaryKey(),
68
+ organizationId: text("organization_id").notNull(),
69
+ shiftId: uuid("shift_id").references(() => posShifts.id, { onDelete: "cascade" }).notNull(),
70
+ terminalId: uuid("terminal_id").references(() => posTerminals.id).notNull(),
71
+ operatorId: text("operator_id").notNull(),
72
+ cartId: uuid("cart_id").notNull(),
73
+ orderId: uuid("order_id"),
74
+ type: text("type", { enum: ["sale", "return", "exchange"] }).notNull().default("sale"),
75
+ status: text("status", { enum: ["open", "held", "completed", "voided"] }).notNull().default("open"),
76
+ customerId: uuid("customer_id"),
77
+ receiptNumber: text("receipt_number").notNull(),
78
+ subtotal: integer("subtotal").notNull().default(0),
79
+ taxTotal: integer("tax_total").notNull().default(0),
80
+ total: integer("total").notNull().default(0),
81
+ discountTotal: integer("discount_total").notNull().default(0),
82
+ holdLabel: text("hold_label"),
83
+ voidReason: text("void_reason"),
84
+ metadata: jsonb("metadata").$type().default({}),
85
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
86
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
87
+ completedAt: timestamp("completed_at", { withTimezone: true }),
88
+ }, (table) => ({
89
+ orgIdx: index("idx_pos_transactions_org").on(table.organizationId),
90
+ shiftIdx: index("idx_pos_transactions_shift").on(table.shiftId),
91
+ statusIdx: index("idx_pos_transactions_status").on(table.status),
92
+ receiptIdx: index("idx_pos_transactions_receipt").on(table.receiptNumber),
93
+ }));
94
+ // ─── Payments (Tenders) ─────────────────────────────────────────────────
95
+ export const posPayments = pgTable("pos_payments", {
96
+ id: uuid("id").defaultRandom().primaryKey(),
97
+ transactionId: uuid("transaction_id").references(() => posTransactions.id, { onDelete: "cascade" }).notNull(),
98
+ method: text("method", { enum: ["cash", "card", "gift_card", "store_credit", "other"] }).notNull(),
99
+ amount: integer("amount").notNull(),
100
+ changeGiven: integer("change_given").notNull().default(0),
101
+ reference: text("reference"),
102
+ status: text("status", { enum: ["collected", "refunded"] }).notNull().default("collected"),
103
+ processedAt: timestamp("processed_at", { withTimezone: true }).defaultNow().notNull(),
104
+ metadata: jsonb("metadata").$type().default({}),
105
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
106
+ }, (table) => ({
107
+ transactionIdx: index("idx_pos_payments_transaction").on(table.transactionId),
108
+ }));
109
+ // ─── Return Items ───────────────────────────────────────────────────────
110
+ export const posReturnItems = pgTable("pos_return_items", {
111
+ id: uuid("id").defaultRandom().primaryKey(),
112
+ transactionId: uuid("transaction_id").references(() => posTransactions.id, { onDelete: "cascade" }).notNull(),
113
+ originalOrderId: uuid("original_order_id").notNull(),
114
+ originalLineItemId: uuid("original_line_item_id").notNull(),
115
+ quantity: integer("quantity").notNull(),
116
+ reason: text("reason", { enum: ["defective", "wrong_item", "changed_mind", "other"] }).notNull(),
117
+ restockingFee: integer("restocking_fee").notNull().default(0),
118
+ refundAmount: integer("refund_amount").notNull(),
119
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
120
+ }, (table) => ({
121
+ transactionIdx: index("idx_pos_return_items_transaction").on(table.transactionId),
122
+ originalOrderIdx: index("idx_pos_return_items_order").on(table.originalOrderId),
123
+ }));
@@ -0,0 +1,38 @@
1
+ import type { PluginResult } from "@unifiedcommerce/core";
2
+ import type { Db } from "../types";
3
+ export interface LookupResult {
4
+ entityId: string;
5
+ variantId: string;
6
+ entityType: string;
7
+ slug: string;
8
+ barcode: string | null;
9
+ sku: string | null;
10
+ title?: string | undefined;
11
+ price?: number | undefined;
12
+ }
13
+ /**
14
+ * Item lookup service for POS.
15
+ * Uses indexed queries on variants.barcode and variants.sku.
16
+ *
17
+ * Depends on core schema tables (variants, sellable_entities, entity_attributes)
18
+ * accessed via the scoped DB proxy.
19
+ */
20
+ export declare class LookupService {
21
+ private db;
22
+ private services;
23
+ constructor(db: Db, services: Record<string, unknown>);
24
+ /**
25
+ * Find entity + variant by barcode. Single indexed query.
26
+ */
27
+ byBarcode(orgId: string, barcode: string): Promise<PluginResult<LookupResult>>;
28
+ /**
29
+ * Find entity + variant by SKU. Single indexed query.
30
+ */
31
+ bySku(orgId: string, sku: string): Promise<PluginResult<LookupResult>>;
32
+ /**
33
+ * Quick text search across entity attributes (name, title).
34
+ * Delegates to the core search service.
35
+ */
36
+ search(orgId: string, query: string): Promise<PluginResult<LookupResult[]>>;
37
+ }
38
+ //# sourceMappingURL=lookup-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lookup-service.d.ts","sourceRoot":"","sources":["../../src/services/lookup-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAEnC,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,qBAAa,aAAa;IAEtB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,QAAQ;gBADR,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAG3C;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAiCpF;;OAEG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAgC5E;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,YAAY,EAAE,CAAC,CAAC;CA8BlF"}
@@ -0,0 +1,104 @@
1
+ import { eq, and } from "drizzle-orm";
2
+ import { Ok, Err } from "@unifiedcommerce/core";
3
+ import { variants, sellableEntities } from "@unifiedcommerce/core/schema";
4
+ /**
5
+ * Item lookup service for POS.
6
+ * Uses indexed queries on variants.barcode and variants.sku.
7
+ *
8
+ * Depends on core schema tables (variants, sellable_entities, entity_attributes)
9
+ * accessed via the scoped DB proxy.
10
+ */
11
+ export class LookupService {
12
+ db;
13
+ services;
14
+ constructor(db, services) {
15
+ this.db = db;
16
+ this.services = services;
17
+ }
18
+ /**
19
+ * Find entity + variant by barcode. Single indexed query.
20
+ */
21
+ async byBarcode(orgId, barcode) {
22
+ const rows = await this.db
23
+ .select({
24
+ variantId: variants.id,
25
+ entityId: variants.entityId,
26
+ barcode: variants.barcode,
27
+ sku: variants.sku,
28
+ entityType: sellableEntities.type,
29
+ slug: sellableEntities.slug,
30
+ orgId: sellableEntities.organizationId,
31
+ })
32
+ .from(variants)
33
+ .innerJoin(sellableEntities, eq(variants.entityId, sellableEntities.id))
34
+ .where(and(eq(variants.barcode, barcode), eq(sellableEntities.organizationId, orgId)))
35
+ .limit(1);
36
+ if (rows.length === 0)
37
+ return Err("No item found for barcode");
38
+ const row = rows[0];
39
+ return Ok({
40
+ entityId: row.entityId,
41
+ variantId: row.variantId,
42
+ entityType: row.entityType,
43
+ slug: row.slug,
44
+ barcode: row.barcode,
45
+ sku: row.sku,
46
+ });
47
+ }
48
+ /**
49
+ * Find entity + variant by SKU. Single indexed query.
50
+ */
51
+ async bySku(orgId, sku) {
52
+ const rows = await this.db
53
+ .select({
54
+ variantId: variants.id,
55
+ entityId: variants.entityId,
56
+ barcode: variants.barcode,
57
+ sku: variants.sku,
58
+ entityType: sellableEntities.type,
59
+ slug: sellableEntities.slug,
60
+ orgId: sellableEntities.organizationId,
61
+ })
62
+ .from(variants)
63
+ .innerJoin(sellableEntities, eq(variants.entityId, sellableEntities.id))
64
+ .where(and(eq(variants.sku, sku), eq(sellableEntities.organizationId, orgId)))
65
+ .limit(1);
66
+ if (rows.length === 0)
67
+ return Err("No item found for SKU");
68
+ const row = rows[0];
69
+ return Ok({
70
+ entityId: row.entityId,
71
+ variantId: row.variantId,
72
+ entityType: row.entityType,
73
+ slug: row.slug,
74
+ barcode: row.barcode,
75
+ sku: row.sku,
76
+ });
77
+ }
78
+ /**
79
+ * Quick text search across entity attributes (name, title).
80
+ * Delegates to the core search service.
81
+ */
82
+ async search(orgId, query) {
83
+ const searchService = this.services.search;
84
+ if (!searchService) {
85
+ return Ok([]);
86
+ }
87
+ const result = await searchService.search({
88
+ query,
89
+ organizationId: orgId,
90
+ limit: 20,
91
+ });
92
+ if (!result.ok || !result.value)
93
+ return Ok([]);
94
+ return Ok(result.value.results.map((r) => ({
95
+ entityId: r.entityId,
96
+ variantId: "",
97
+ entityType: r.type,
98
+ slug: r.slug,
99
+ barcode: null,
100
+ sku: null,
101
+ title: r.title ?? undefined,
102
+ })));
103
+ }
104
+ }
@@ -0,0 +1,40 @@
1
+ import type { PluginResult } from "@unifiedcommerce/core";
2
+ import type { Db, Payment, Transaction } from "../types";
3
+ export declare class PaymentService {
4
+ private db;
5
+ private transaction;
6
+ constructor(db: Db, transaction: (fn: (tx: Db) => Promise<unknown>) => Promise<unknown>);
7
+ /**
8
+ * Add a payment to a transaction. Does NOT finalize.
9
+ * Supports split payment (multiple calls per transaction).
10
+ */
11
+ addPayment(orgId: string, transactionId: string, input: {
12
+ method: "cash" | "card" | "gift_card" | "store_credit" | "other";
13
+ amount: number;
14
+ changeGiven?: number;
15
+ reference?: string;
16
+ metadata?: Record<string, unknown>;
17
+ }): Promise<PluginResult<Payment>>;
18
+ /**
19
+ * Get total payments collected for a transaction.
20
+ */
21
+ getPaymentTotal(transactionId: string): Promise<number>;
22
+ /**
23
+ * List all payments for a transaction.
24
+ */
25
+ listPayments(transactionId: string): Promise<PluginResult<Payment[]>>;
26
+ /**
27
+ * Validate that total payments cover the transaction total, then return
28
+ * the transaction details needed for checkout.
29
+ */
30
+ validateForCompletion(orgId: string, transactionId: string): Promise<PluginResult<{
31
+ transaction: Transaction;
32
+ payments: Payment[];
33
+ totalPaid: number;
34
+ }>>;
35
+ /**
36
+ * Mark payments as refunded for a transaction.
37
+ */
38
+ refundPayments(transactionId: string, tx?: Db): Promise<void>;
39
+ }
40
+ //# sourceMappingURL=payment-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-service.d.ts","sourceRoot":"","sources":["../../src/services/payment-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzD,qBAAa,cAAc;IAEvB,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;IAG7E;;;OAGG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE;QAC5D,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAC;QACjE,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAiClC;;OAEG;IACG,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAc7D;;OAEG;IACG,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC;IAS3E;;;OAGG;IACG,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QACtF,WAAW,EAAE,WAAW,CAAC;QACzB,QAAQ,EAAE,OAAO,EAAE,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IA8BH;;OAEG;IACG,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAOpE"}
@@ -0,0 +1,99 @@
1
+ import { eq, and, sql } from "drizzle-orm";
2
+ import { Ok, Err } from "@unifiedcommerce/core";
3
+ import { posPayments, posTransactions } from "../schema";
4
+ export class PaymentService {
5
+ db;
6
+ transaction;
7
+ constructor(db, transaction) {
8
+ this.db = db;
9
+ this.transaction = transaction;
10
+ }
11
+ /**
12
+ * Add a payment to a transaction. Does NOT finalize.
13
+ * Supports split payment (multiple calls per transaction).
14
+ */
15
+ async addPayment(orgId, transactionId, input) {
16
+ if (input.amount <= 0)
17
+ return Err("Payment amount must be positive");
18
+ // Verify transaction is open
19
+ const txns = await this.db
20
+ .select()
21
+ .from(posTransactions)
22
+ .where(and(eq(posTransactions.id, transactionId), eq(posTransactions.organizationId, orgId)));
23
+ if (txns.length === 0)
24
+ return Err("Transaction not found");
25
+ const txn = txns[0];
26
+ if (txn.status !== "open")
27
+ return Err("Transaction is not open");
28
+ const rows = await this.db
29
+ .insert(posPayments)
30
+ .values({
31
+ transactionId,
32
+ method: input.method,
33
+ amount: input.amount,
34
+ changeGiven: input.changeGiven ?? 0,
35
+ reference: input.reference,
36
+ status: "collected",
37
+ processedAt: new Date(),
38
+ metadata: input.metadata ?? {},
39
+ })
40
+ .returning();
41
+ return Ok(rows[0]);
42
+ }
43
+ /**
44
+ * Get total payments collected for a transaction.
45
+ */
46
+ async getPaymentTotal(transactionId) {
47
+ const rows = await this.db
48
+ .select({
49
+ total: sql `COALESCE(SUM(${posPayments.amount} - ${posPayments.changeGiven}), 0)`.as("total"),
50
+ })
51
+ .from(posPayments)
52
+ .where(and(eq(posPayments.transactionId, transactionId), eq(posPayments.status, "collected")));
53
+ return Number(rows[0]?.total ?? 0);
54
+ }
55
+ /**
56
+ * List all payments for a transaction.
57
+ */
58
+ async listPayments(transactionId) {
59
+ const rows = await this.db
60
+ .select()
61
+ .from(posPayments)
62
+ .where(eq(posPayments.transactionId, transactionId));
63
+ return Ok(rows);
64
+ }
65
+ /**
66
+ * Validate that total payments cover the transaction total, then return
67
+ * the transaction details needed for checkout.
68
+ */
69
+ async validateForCompletion(orgId, transactionId) {
70
+ const txns = await this.db
71
+ .select()
72
+ .from(posTransactions)
73
+ .where(and(eq(posTransactions.id, transactionId), eq(posTransactions.organizationId, orgId)));
74
+ if (txns.length === 0)
75
+ return Err("Transaction not found");
76
+ const txn = txns[0];
77
+ if (txn.status !== "open")
78
+ return Err(`Transaction is ${txn.status}, not open`);
79
+ const payments = await this.db
80
+ .select()
81
+ .from(posPayments)
82
+ .where(and(eq(posPayments.transactionId, transactionId), eq(posPayments.status, "collected")));
83
+ const totalPaid = payments.reduce((sum, p) => sum + p.amount - p.changeGiven, 0);
84
+ if (txn.total > 0 && totalPaid < txn.total) {
85
+ return Err(`Insufficient payment: ${totalPaid} paid, ${txn.total} required`);
86
+ }
87
+ return Ok({ transaction: txn, payments, totalPaid });
88
+ }
89
+ /**
90
+ * Mark payments as refunded for a transaction.
91
+ */
92
+ async refundPayments(transactionId, tx) {
93
+ const db = tx ?? this.db;
94
+ await db
95
+ .update(posPayments)
96
+ .set({ status: "refunded" })
97
+ .where(eq(posPayments.transactionId, transactionId));
98
+ }
99
+ }
@@ -0,0 +1,45 @@
1
+ import type { PluginResult } from "@unifiedcommerce/core";
2
+ import type { Db } from "../types";
3
+ export interface ReceiptData {
4
+ receiptNumber: string;
5
+ transactionId: string;
6
+ terminalCode: string;
7
+ operatorName: string;
8
+ timestamp: Date;
9
+ lineItems: Array<{
10
+ title: string;
11
+ quantity: number;
12
+ unitPrice: number;
13
+ totalPrice: number;
14
+ notes?: string | null;
15
+ }>;
16
+ subtotal: number;
17
+ discountTotal: number;
18
+ taxTotal: number;
19
+ total: number;
20
+ payments: Array<{
21
+ method: string;
22
+ amount: number;
23
+ changeGiven: number;
24
+ reference?: string | null;
25
+ }>;
26
+ changeDue: number;
27
+ customerId?: string | null;
28
+ }
29
+ export declare class ReceiptService {
30
+ private db;
31
+ private services;
32
+ constructor(db: Db, services: Record<string, unknown>);
33
+ /**
34
+ * Assemble full receipt data for a completed transaction.
35
+ */
36
+ getReceipt(orgId: string, transactionId: string): Promise<PluginResult<ReceiptData>>;
37
+ /**
38
+ * Send receipt via email (delegates to email service).
39
+ */
40
+ emailReceipt(orgId: string, transactionId: string, email: string): Promise<PluginResult<{
41
+ sent: boolean;
42
+ }>>;
43
+ private formatReceiptHtml;
44
+ }
45
+ //# sourceMappingURL=receipt-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt-service.d.ts","sourceRoot":"","sources":["../../src/services/receipt-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAwB,MAAM,UAAU,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,KAAK,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,CAAC,CAAC;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,KAAK,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,CAAC,CAAC;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,qBAAa,cAAc;IAEvB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,QAAQ;gBADR,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAG3C;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IAkF1F;;OAEG;IACG,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAqBjH,OAAO,CAAC,iBAAiB;CAoB1B"}
@@ -0,0 +1,119 @@
1
+ import { eq, and } from "drizzle-orm";
2
+ import { Ok, Err } from "@unifiedcommerce/core";
3
+ import { posTransactions, posPayments } from "../schema";
4
+ export class ReceiptService {
5
+ db;
6
+ services;
7
+ constructor(db, services) {
8
+ this.db = db;
9
+ this.services = services;
10
+ }
11
+ /**
12
+ * Assemble full receipt data for a completed transaction.
13
+ */
14
+ async getReceipt(orgId, transactionId) {
15
+ // Get transaction
16
+ const txns = await this.db
17
+ .select()
18
+ .from(posTransactions)
19
+ .where(and(eq(posTransactions.id, transactionId), eq(posTransactions.organizationId, orgId)));
20
+ if (txns.length === 0)
21
+ return Err("Transaction not found");
22
+ const txn = txns[0];
23
+ if (txn.status !== "completed" && txn.status !== "voided") {
24
+ return Err("Receipt is only available for completed or voided transactions");
25
+ }
26
+ // Get terminal code
27
+ const { posTerminals } = await import("../schema");
28
+ const terminals = await this.db
29
+ .select({ code: posTerminals.code, name: posTerminals.name })
30
+ .from(posTerminals)
31
+ .where(eq(posTerminals.id, txn.terminalId));
32
+ const terminalCode = terminals[0]?.code ?? "POS";
33
+ // Get payments
34
+ const payments = await this.db
35
+ .select()
36
+ .from(posPayments)
37
+ .where(eq(posPayments.transactionId, transactionId));
38
+ // Get order line items from core
39
+ let lineItems = [];
40
+ if (txn.orderId) {
41
+ const orders = this.services.orders;
42
+ if (orders) {
43
+ const orderResult = await orders.getById(txn.orderId, null);
44
+ if (orderResult.ok && orderResult.value?.lineItems) {
45
+ lineItems = orderResult.value.lineItems.map((li) => ({
46
+ title: li.title ?? "Item",
47
+ quantity: li.quantity,
48
+ unitPrice: li.unitPrice ?? 0,
49
+ totalPrice: li.totalPrice ?? 0,
50
+ }));
51
+ }
52
+ }
53
+ }
54
+ // Calculate change due
55
+ const totalPaid = payments
56
+ .filter((p) => p.status === "collected")
57
+ .reduce((sum, p) => sum + p.amount, 0);
58
+ const changeDue = payments
59
+ .filter((p) => p.status === "collected")
60
+ .reduce((sum, p) => sum + p.changeGiven, 0);
61
+ return Ok({
62
+ receiptNumber: txn.receiptNumber,
63
+ transactionId: txn.id,
64
+ terminalCode,
65
+ operatorName: txn.operatorId,
66
+ timestamp: txn.completedAt ?? txn.createdAt,
67
+ lineItems,
68
+ subtotal: txn.subtotal,
69
+ discountTotal: txn.discountTotal,
70
+ taxTotal: txn.taxTotal,
71
+ total: txn.total,
72
+ payments: payments.map((p) => ({
73
+ method: p.method,
74
+ amount: p.amount,
75
+ changeGiven: p.changeGiven,
76
+ reference: p.reference,
77
+ })),
78
+ changeDue,
79
+ customerId: txn.customerId,
80
+ });
81
+ }
82
+ /**
83
+ * Send receipt via email (delegates to email service).
84
+ */
85
+ async emailReceipt(orgId, transactionId, email) {
86
+ const receiptResult = await this.getReceipt(orgId, transactionId);
87
+ if (!receiptResult.ok)
88
+ return receiptResult;
89
+ const emailService = this.services.email;
90
+ if (!emailService?.send) {
91
+ return Err("Email service not configured");
92
+ }
93
+ await emailService.send({
94
+ to: email,
95
+ subject: `Receipt ${receiptResult.value.receiptNumber}`,
96
+ html: this.formatReceiptHtml(receiptResult.value),
97
+ });
98
+ return Ok({ sent: true });
99
+ }
100
+ formatReceiptHtml(receipt) {
101
+ const lines = receipt.lineItems
102
+ .map((li) => `<tr><td>${li.title}</td><td>${li.quantity}</td><td>${(li.unitPrice / 100).toFixed(2)}</td><td>${(li.totalPrice / 100).toFixed(2)}</td></tr>`)
103
+ .join("");
104
+ return `
105
+ <h2>Receipt ${receipt.receiptNumber}</h2>
106
+ <p>Date: ${receipt.timestamp.toISOString()}</p>
107
+ <table>
108
+ <tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>
109
+ ${lines}
110
+ </table>
111
+ <p>Subtotal: ${(receipt.subtotal / 100).toFixed(2)}</p>
112
+ ${receipt.discountTotal > 0 ? `<p>Discount: -${(receipt.discountTotal / 100).toFixed(2)}</p>` : ""}
113
+ <p>Tax: ${(receipt.taxTotal / 100).toFixed(2)}</p>
114
+ <p><strong>Total: ${(receipt.total / 100).toFixed(2)}</strong></p>
115
+ ${receipt.payments.map((p) => `<p>${p.method}: ${(p.amount / 100).toFixed(2)}</p>`).join("")}
116
+ ${receipt.changeDue > 0 ? `<p>Change: ${(receipt.changeDue / 100).toFixed(2)}</p>` : ""}
117
+ `;
118
+ }
119
+ }
@@ -0,0 +1,27 @@
1
+ import type { PluginResult } from "@unifiedcommerce/core";
2
+ import type { Db, ReturnItem } from "../types";
3
+ export declare class ReturnService {
4
+ private db;
5
+ constructor(db: Db);
6
+ /**
7
+ * Record return items linking back to an original order's line items.
8
+ * Called after a return transaction is created.
9
+ */
10
+ addReturnItems(transactionId: string, items: Array<{
11
+ originalOrderId: string;
12
+ originalLineItemId: string;
13
+ quantity: number;
14
+ reason: "defective" | "wrong_item" | "changed_mind" | "other";
15
+ restockingFee?: number;
16
+ refundAmount: number;
17
+ }>): Promise<PluginResult<ReturnItem[]>>;
18
+ /**
19
+ * Get all return items for a return transaction.
20
+ */
21
+ getReturnItems(transactionId: string): Promise<PluginResult<ReturnItem[]>>;
22
+ /**
23
+ * Calculate total refund amount for a return transaction.
24
+ */
25
+ calculateRefundTotal(transactionId: string): Promise<number>;
26
+ }
27
+ //# sourceMappingURL=return-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"return-service.d.ts","sourceRoot":"","sources":["../../src/services/return-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE/C,qBAAa,aAAa;IACZ,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAE1B;;;OAGG;IACG,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC;QACvD,eAAe,EAAE,MAAM,CAAC;QACxB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,WAAW,GAAG,YAAY,GAAG,cAAc,GAAG,OAAO,CAAC;QAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC,CAAC;IAqBxC;;OAEG;IACG,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC,CAAC;IAShF;;OAEG;IACG,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAQnE"}