@porulle/plugin-pos 0.7.0 → 0.9.0

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 (38) hide show
  1. package/dist/hooks/checkout-pos.d.ts.map +1 -1
  2. package/dist/hooks/checkout-pos.js +9 -2
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +25 -1
  6. package/dist/routes/auth.d.ts +19 -0
  7. package/dist/routes/auth.d.ts.map +1 -0
  8. package/dist/routes/auth.js +66 -0
  9. package/dist/routes/exchanges.d.ts +15 -0
  10. package/dist/routes/exchanges.d.ts.map +1 -0
  11. package/dist/routes/exchanges.js +42 -0
  12. package/dist/routes/returns.d.ts.map +1 -1
  13. package/dist/routes/returns.js +25 -4
  14. package/dist/schema.d.ts +235 -0
  15. package/dist/schema.d.ts.map +1 -1
  16. package/dist/schema.js +25 -0
  17. package/dist/services/exchange-service.d.ts +55 -0
  18. package/dist/services/exchange-service.d.ts.map +1 -0
  19. package/dist/services/exchange-service.js +168 -0
  20. package/dist/services/pin-service.d.ts +73 -0
  21. package/dist/services/pin-service.d.ts.map +1 -0
  22. package/dist/services/pin-service.js +269 -0
  23. package/dist/services/transaction-service.d.ts.map +1 -1
  24. package/dist/services/transaction-service.js +5 -5
  25. package/dist/types.d.ts +16 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js +1 -0
  28. package/package.json +3 -3
  29. package/src/hooks/checkout-pos.ts +9 -2
  30. package/src/index.ts +33 -0
  31. package/src/routes/auth.ts +72 -0
  32. package/src/routes/exchanges.ts +53 -0
  33. package/src/routes/returns.ts +47 -5
  34. package/src/schema.ts +29 -0
  35. package/src/services/exchange-service.ts +272 -0
  36. package/src/services/pin-service.ts +350 -0
  37. package/src/services/transaction-service.ts +5 -4
  38. package/src/types.ts +20 -0
@@ -1 +1 @@
1
- {"version":3,"file":"checkout-pos.d.ts","sourceRoot":"","sources":["../../src/hooks/checkout-pos.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,UAAU,YAAY;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAaD;;GAEG;AACH,wBAAgB,oBAAoB;;uBAGP,OAAO,EAAE;EAWrC;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE;;uBAG3B,OAAO,EAAE;EAsCrC"}
1
+ {"version":3,"file":"checkout-pos.d.ts","sourceRoot":"","sources":["../../src/hooks/checkout-pos.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,UAAU,YAAY;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAaD;;GAEG;AACH,wBAAgB,oBAAoB;;uBAGP,OAAO,EAAE;EAWrC;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE;;uBAG3B,OAAO,EAAE;EA6CrC"}
@@ -4,7 +4,7 @@
4
4
  * checkout.beforePayment: Sets shippingTotal = 0 for POS transactions (no shipping).
5
5
  * checkout.afterCreate: Updates POS transaction with orderId, increments shift counters.
6
6
  */
7
- import { eq, sql } from "@porulle/core/drizzle";
7
+ import { eq, and, sql } from "@porulle/core/drizzle";
8
8
  import { posTransactions, posShifts } from "../schema.js";
9
9
  /**
10
10
  * Before payment hook: zero out shipping for POS transactions.
@@ -55,6 +55,13 @@ export function buildPOSFinalizationHook(getDb) {
55
55
  // Increment shift sales counters
56
56
  const posShiftId = metadata?.posShiftId;
57
57
  if (posShiftId) {
58
+ const txnRows = await db
59
+ .select({ organizationId: posTransactions.organizationId })
60
+ .from(posTransactions)
61
+ .where(eq(posTransactions.id, posTransactionId));
62
+ const orgId = txnRows[0]?.organizationId;
63
+ if (!orgId)
64
+ return;
58
65
  await db
59
66
  .update(posShifts)
60
67
  .set({
@@ -62,7 +69,7 @@ export function buildPOSFinalizationHook(getDb) {
62
69
  salesTotal: sql `${posShifts.salesTotal} + ${result.grandTotal ?? 0}`,
63
70
  updatedAt: new Date(),
64
71
  })
65
- .where(eq(posShifts.id, posShiftId));
72
+ .where(and(eq(posShifts.id, posShiftId), eq(posShifts.organizationId, orgId)));
66
73
  }
67
74
  },
68
75
  };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export { PaymentService } from "./services/payment-service.js";
7
7
  export { ReturnService } from "./services/return-service.js";
8
8
  export { LookupService } from "./services/lookup-service.js";
9
9
  export { ReceiptService } from "./services/receipt-service.js";
10
+ export { PinService, hashPin, verifyPinHash } from "./services/pin-service.js";
11
+ export { ExchangeService } from "./services/exchange-service.js";
10
12
  export { createPOSPaymentAdapter } from "./payment-adapter.js";
11
13
  /**
12
14
  * POS Plugin — Tier 0 Core Primitives
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,gBAAgB,EAAM,MAAM,YAAY,CAAC;AAGvD,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,WAAW,GAAE,gBAAqB,0CA+E3D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,gBAAgB,EAAM,MAAM,YAAY,CAAC;AAGvD,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,WAAW,GAAE,gBAAqB,0CAwG3D"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCommercePlugin } from "@porulle/core";
2
- import { posTerminals, posShifts, posCashEvents, posTransactions, posPayments, posReturnItems, } from "./schema.js";
2
+ import { posTerminals, posShifts, posCashEvents, posTransactions, posPayments, posReturnItems, posOperatorPins, posPinAttempts, } from "./schema.js";
3
3
  import { TerminalService } from "./services/terminal-service.js";
4
4
  import { ShiftService } from "./services/shift-service.js";
5
5
  import { TransactionService } from "./services/transaction-service.js";
@@ -7,6 +7,8 @@ import { PaymentService } from "./services/payment-service.js";
7
7
  import { ReturnService } from "./services/return-service.js";
8
8
  import { LookupService } from "./services/lookup-service.js";
9
9
  import { ReceiptService } from "./services/receipt-service.js";
10
+ import { PinService } from "./services/pin-service.js";
11
+ import { ExchangeService } from "./services/exchange-service.js";
10
12
  import { buildTerminalRoutes } from "./routes/terminals.js";
11
13
  import { buildShiftRoutes } from "./routes/shifts.js";
12
14
  import { buildTransactionRoutes } from "./routes/transactions.js";
@@ -14,6 +16,8 @@ import { buildPaymentRoutes } from "./routes/payments.js";
14
16
  import { buildReturnRoutes } from "./routes/returns.js";
15
17
  import { buildLookupRoutes } from "./routes/lookup.js";
16
18
  import { buildReceiptRoutes } from "./routes/receipts.js";
19
+ import { buildPinAuthRoutes } from "./routes/auth.js";
20
+ import { buildExchangeRoutes } from "./routes/exchanges.js";
17
21
  import { buildPOSShippingHook, buildPOSFinalizationHook } from "./hooks/checkout-pos.js";
18
22
  import { DEFAULT_POS_OPTIONS } from "./types.js";
19
23
  export { TerminalService } from "./services/terminal-service.js";
@@ -23,6 +27,8 @@ export { PaymentService } from "./services/payment-service.js";
23
27
  export { ReturnService } from "./services/return-service.js";
24
28
  export { LookupService } from "./services/lookup-service.js";
25
29
  export { ReceiptService } from "./services/receipt-service.js";
30
+ export { PinService, hashPin, verifyPinHash } from "./services/pin-service.js";
31
+ export { ExchangeService } from "./services/exchange-service.js";
26
32
  export { createPOSPaymentAdapter } from "./payment-adapter.js";
27
33
  /**
28
34
  * POS Plugin — Tier 0 Core Primitives
@@ -69,6 +75,20 @@ export function posPlugin(userOptions = {}) {
69
75
  posTransactions,
70
76
  posPayments,
71
77
  posReturnItems,
78
+ posOperatorPins,
79
+ posPinAttempts,
80
+ }),
81
+ // Scope used by PIN login to mint per-shift credentials (issue #51).
82
+ apiKeyScopes: () => ({
83
+ [options.pinAuth.apiKeyScope ?? "pos"]: {
84
+ prefix: "pos_shift_",
85
+ description: "Short-lived per-shift POS credentials minted by PIN login.",
86
+ permissions: { pos: ["operate"] },
87
+ enableMetadata: true,
88
+ // Allow credentials as short as 15 minutes (Better Auth default
89
+ // minimum is 1 day — too long for per-shift keys).
90
+ keyExpiration: { minExpiresIn: 1 / 96 },
91
+ },
72
92
  }),
73
93
  hooks: () => [
74
94
  buildPOSShippingHook(),
@@ -93,6 +113,8 @@ export function posPlugin(userOptions = {}) {
93
113
  const returnService = new ReturnService(db);
94
114
  const lookupService = new LookupService(db, ctx.services);
95
115
  const receiptService = new ReceiptService(db, ctx.services);
116
+ const pinService = new PinService(db, options.pinAuth);
117
+ const exchangeService = new ExchangeService(db, ctx.services, transactionFn, transactionService, returnService);
96
118
  // Cart service from core (for creating POS transaction carts)
97
119
  const cartService = ctx.services.cart;
98
120
  return [
@@ -103,6 +125,8 @@ export function posPlugin(userOptions = {}) {
103
125
  ...buildReturnRoutes(returnService, transactionService, paymentService, cartService, ctx),
104
126
  ...buildLookupRoutes(lookupService, ctx),
105
127
  ...buildReceiptRoutes(receiptService, ctx),
128
+ ...buildPinAuthRoutes(pinService, ctx),
129
+ ...buildExchangeRoutes(exchangeService, ctx),
106
130
  ];
107
131
  },
108
132
  });
@@ -0,0 +1,19 @@
1
+ import { type PinService } from "../services/pin-service.js";
2
+ import type { PluginRouteRegistration } from "@porulle/core";
3
+ /**
4
+ * PIN auth runtime (issue #51).
5
+ *
6
+ * - PUT /pos/auth/pin — set/rotate an operator's PIN (pos:admin)
7
+ * - POST /pos/auth/pin-login — operator PIN → short-lived per-shift API key.
8
+ * Authenticated by the terminal's device credential (pos:operate); the
9
+ * minted key is personal and carries pos:operate + shift/terminal metadata.
10
+ * - POST /pos/auth/override — manager PIN approves one elevated action.
11
+ */
12
+ export declare function buildPinAuthRoutes(service: PinService, ctx: {
13
+ services?: Record<string, unknown>;
14
+ database?: {
15
+ db: unknown;
16
+ };
17
+ auth?: unknown;
18
+ }): PluginRouteRegistration[];
19
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqB,KAAK,UAAU,EAAmB,MAAM,4BAA4B,CAAC;AACjG,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAO7D;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,UAAU,EACnB,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GACtF,uBAAuB,EAAE,CAiD3B"}
@@ -0,0 +1,66 @@
1
+ import { router, CommerceValidationError } from "@porulle/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import { PIN_LOCKOUT_ERROR } from "../services/pin-service.js";
4
+ function throwPinRouteError(message) {
5
+ if (message === PIN_LOCKOUT_ERROR)
6
+ throw new CommerceValidationError(message);
7
+ throw new Error(message);
8
+ }
9
+ /**
10
+ * PIN auth runtime (issue #51).
11
+ *
12
+ * - PUT /pos/auth/pin — set/rotate an operator's PIN (pos:admin)
13
+ * - POST /pos/auth/pin-login — operator PIN → short-lived per-shift API key.
14
+ * Authenticated by the terminal's device credential (pos:operate); the
15
+ * minted key is personal and carries pos:operate + shift/terminal metadata.
16
+ * - POST /pos/auth/override — manager PIN approves one elevated action.
17
+ */
18
+ export function buildPinAuthRoutes(service, ctx) {
19
+ const r = router("POS Auth", "/pos/auth", ctx);
20
+ r.put("/pin")
21
+ .summary("Set or rotate an operator PIN")
22
+ .permission("pos:admin")
23
+ .input(z.object({
24
+ operatorId: z.string().min(1),
25
+ pin: z.string().regex(/^\d{4,8}$/, "PIN must be 4-8 digits"),
26
+ canOverride: z.boolean().optional(),
27
+ }))
28
+ .handler(async ({ input, orgId }) => {
29
+ const body = input;
30
+ const result = await service.setPin(orgId, body);
31
+ if (!result.ok)
32
+ throwPinRouteError(result.error);
33
+ return result.value;
34
+ });
35
+ r.post("/pin-login")
36
+ .summary("PIN login — mint a short-lived per-shift credential")
37
+ .permission("pos:operate")
38
+ .input(z.object({
39
+ operatorId: z.string().min(1),
40
+ pin: z.string().regex(/^\d{4,8}$/),
41
+ shiftId: z.string().uuid().optional(),
42
+ }))
43
+ .handler(async ({ input, orgId }) => {
44
+ const body = input;
45
+ const result = await service.pinLogin(orgId, body, ctx.auth);
46
+ if (!result.ok)
47
+ throwPinRouteError(result.error);
48
+ return result.value;
49
+ });
50
+ r.post("/override")
51
+ .summary("Manager override — approve one elevated action by PIN")
52
+ .permission("pos:operate")
53
+ .input(z.object({
54
+ operatorId: z.string().min(1),
55
+ pin: z.string().regex(/^\d{4,8}$/),
56
+ action: z.string().min(1).max(200),
57
+ }))
58
+ .handler(async ({ input, orgId }) => {
59
+ const body = input;
60
+ const result = await service.override(orgId, body);
61
+ if (!result.ok)
62
+ throwPinRouteError(result.error);
63
+ return result.value;
64
+ });
65
+ return r.routes();
66
+ }
@@ -0,0 +1,15 @@
1
+ import type { ExchangeService } from "../services/exchange-service.js";
2
+ import type { PluginRouteRegistration } from "@porulle/core";
3
+ /**
4
+ * Exchanges (issue #53): POST /pos/exchanges — return + replacement order +
5
+ * net delta in one call. Requires pos:manage (like returns) plus core order
6
+ * scopes on the actor (orders:update for the refund, orders:create for the
7
+ * replacement) since the exchange moves money on core orders.
8
+ */
9
+ export declare function buildExchangeRoutes(service: ExchangeService, ctx: {
10
+ services?: Record<string, unknown>;
11
+ database?: {
12
+ db: unknown;
13
+ };
14
+ }): PluginRouteRegistration[];
15
+ //# sourceMappingURL=exchanges.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exchanges.d.ts","sourceRoot":"","sources":["../../src/routes/exchanges.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,iCAAiC,CAAC;AACtF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACtE,uBAAuB,EAAE,CAsC3B"}
@@ -0,0 +1,42 @@
1
+ import { router } from "@porulle/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ /**
4
+ * Exchanges (issue #53): POST /pos/exchanges — return + replacement order +
5
+ * net delta in one call. Requires pos:manage (like returns) plus core order
6
+ * scopes on the actor (orders:update for the refund, orders:create for the
7
+ * replacement) since the exchange moves money on core orders.
8
+ */
9
+ export function buildExchangeRoutes(service, ctx) {
10
+ const r = router("POS Exchanges", "/pos/exchanges", ctx);
11
+ r.post("/")
12
+ .summary("Exchange — return lines + replacement order in one atomic operation")
13
+ .permission("pos:manage")
14
+ .input(z.object({
15
+ shiftId: z.string().uuid(),
16
+ terminalId: z.string().uuid(),
17
+ originalOrderId: z.string().uuid(),
18
+ currency: z.string().length(3).optional(),
19
+ customerId: z.string().uuid().optional(),
20
+ returnItems: z.array(z.object({
21
+ originalLineItemId: z.string().uuid(),
22
+ quantity: z.number().int().positive(),
23
+ reason: z.enum(["defective", "wrong_item", "changed_mind", "other"]),
24
+ })).min(1),
25
+ replacementItems: z.array(z.object({
26
+ entityId: z.string().uuid(),
27
+ variantId: z.string().uuid().optional(),
28
+ sku: z.string().optional(),
29
+ title: z.string().min(1),
30
+ quantity: z.number().int().positive(),
31
+ unitPrice: z.number().int().min(0),
32
+ taxAmount: z.number().int().min(0).optional(),
33
+ })).min(1),
34
+ }))
35
+ .handler(async ({ input, actor, orgId }) => {
36
+ const result = await service.exchange(orgId, input, actor);
37
+ if (!result.ok)
38
+ throw new Error(result.error);
39
+ return result.value;
40
+ });
41
+ return r.routes();
42
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"returns.d.ts","sourceRoot":"","sources":["../../src/routes/returns.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,aAAa,EAC5B,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE;IAAE,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAA;CAAE,EAC/J,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACtE,uBAAuB,EAAE,CAsG3B"}
1
+ {"version":3,"file":"returns.d.ts","sourceRoot":"","sources":["../../src/routes/returns.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,aAAa,EAC5B,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,cAAc,EAC9B,WAAW,EAAE;IAAE,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAA;CAAE,EAC/J,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACtE,uBAAuB,EAAE,CAgJ3B"}
@@ -14,7 +14,8 @@ export function buildReturnRoutes(returnService, transactionService, paymentServ
14
14
  quantity: z.number().int().positive(),
15
15
  reason: z.enum(["defective", "wrong_item", "changed_mind", "other"]),
16
16
  restockingFee: z.number().int().min(0).optional(),
17
- refundAmount: z.number().int().positive(),
17
+ // refundAmount is NOT accepted from the client — it is computed
18
+ // server-side from the original order (SEC-08).
18
19
  })).min(1),
19
20
  }))
20
21
  .handler(async ({ input, actor, orgId }) => {
@@ -34,15 +35,35 @@ export function buildReturnRoutes(returnService, transactionService, paymentServ
34
35
  });
35
36
  if (!txnResult.ok)
36
37
  throw new Error(txnResult.error);
37
- // Record return items
38
+ // SEC-08: derive the refund server-side via orders.refundLines (the same
39
+ // primitive the exchange flow uses). It validates the order + line items
40
+ // belong to this org, enforces the per-line refundable quantity
41
+ // (quantity - refundedQuantity) and updates that ledger — so a fabricated
42
+ // or foreign order, an over-refund, or a repeat refund of the same units
43
+ // is rejected — and computes the amount from the recorded sale price. The
44
+ // client can no longer set the refund amount.
45
+ const orders = ctx.services?.orders;
46
+ if (!orders?.refundLines) {
47
+ throw new Error("Orders service unavailable for return refund");
48
+ }
49
+ const refundResult = await orders.refundLines(body.originalOrderId, {
50
+ lines: body.items.map((i) => ({ lineItemId: i.originalLineItemId, quantity: i.quantity })),
51
+ reason: "pos_return",
52
+ }, actor);
53
+ if (!refundResult.ok || !refundResult.value) {
54
+ throw new Error(refundResult.error?.message ??
55
+ "Return could not be validated against the original order.");
56
+ }
57
+ const serverAmountByLine = new Map(refundResult.value.refund.lines.map((l) => [l.lineItemId, l.amount]));
58
+ const refundTotal = refundResult.value.refund.amount;
59
+ // Record return items with the SERVER-computed refund amounts
38
60
  const itemsResult = await returnService.addReturnItems(txnResult.value.id, body.items.map((item) => ({
39
61
  ...item,
40
62
  originalOrderId: body.originalOrderId,
63
+ refundAmount: serverAmountByLine.get(item.originalLineItemId) ?? 0,
41
64
  })));
42
65
  if (!itemsResult.ok)
43
66
  throw new Error(itemsResult.error);
44
- // Update transaction totals
45
- const refundTotal = body.items.reduce((sum, i) => sum + i.refundAmount, 0);
46
67
  await transactionService.updateTotals(txnResult.value.id, {
47
68
  subtotal: refundTotal,
48
69
  taxTotal: 0,
package/dist/schema.d.ts CHANGED
@@ -1174,6 +1174,241 @@ export declare const posPayments: import("drizzle-orm/pg-core/table").PgTableWit
1174
1174
  };
1175
1175
  dialect: "pg";
1176
1176
  }>;
1177
+ export declare const posOperatorPins: import("drizzle-orm/pg-core/table").PgTableWithColumns<{
1178
+ name: "pos_operator_pins";
1179
+ schema: undefined;
1180
+ columns: {
1181
+ id: import("@porulle/core/drizzle").PgColumn<{
1182
+ name: "id";
1183
+ tableName: "pos_operator_pins";
1184
+ dataType: "string";
1185
+ columnType: "PgUUID";
1186
+ data: string;
1187
+ driverParam: string;
1188
+ notNull: true;
1189
+ hasDefault: true;
1190
+ isPrimaryKey: true;
1191
+ isAutoincrement: false;
1192
+ hasRuntimeDefault: false;
1193
+ enumValues: undefined;
1194
+ baseColumn: never;
1195
+ identity: undefined;
1196
+ generated: undefined;
1197
+ }, {}, {}>;
1198
+ organizationId: import("@porulle/core/drizzle").PgColumn<{
1199
+ name: "organization_id";
1200
+ tableName: "pos_operator_pins";
1201
+ dataType: "string";
1202
+ columnType: "PgText";
1203
+ data: string;
1204
+ driverParam: string;
1205
+ notNull: true;
1206
+ hasDefault: false;
1207
+ isPrimaryKey: false;
1208
+ isAutoincrement: false;
1209
+ hasRuntimeDefault: false;
1210
+ enumValues: [string, ...string[]];
1211
+ baseColumn: never;
1212
+ identity: undefined;
1213
+ generated: undefined;
1214
+ }, {}, {}>;
1215
+ operatorId: import("@porulle/core/drizzle").PgColumn<{
1216
+ name: "operator_id";
1217
+ tableName: "pos_operator_pins";
1218
+ dataType: "string";
1219
+ columnType: "PgText";
1220
+ data: string;
1221
+ driverParam: string;
1222
+ notNull: true;
1223
+ hasDefault: false;
1224
+ isPrimaryKey: false;
1225
+ isAutoincrement: false;
1226
+ hasRuntimeDefault: false;
1227
+ enumValues: [string, ...string[]];
1228
+ baseColumn: never;
1229
+ identity: undefined;
1230
+ generated: undefined;
1231
+ }, {}, {}>;
1232
+ pinHash: import("@porulle/core/drizzle").PgColumn<{
1233
+ name: "pin_hash";
1234
+ tableName: "pos_operator_pins";
1235
+ dataType: "string";
1236
+ columnType: "PgText";
1237
+ data: string;
1238
+ driverParam: string;
1239
+ notNull: true;
1240
+ hasDefault: false;
1241
+ isPrimaryKey: false;
1242
+ isAutoincrement: false;
1243
+ hasRuntimeDefault: false;
1244
+ enumValues: [string, ...string[]];
1245
+ baseColumn: never;
1246
+ identity: undefined;
1247
+ generated: undefined;
1248
+ }, {}, {}>;
1249
+ canOverride: import("@porulle/core/drizzle").PgColumn<{
1250
+ name: "can_override";
1251
+ tableName: "pos_operator_pins";
1252
+ dataType: "boolean";
1253
+ columnType: "PgBoolean";
1254
+ data: boolean;
1255
+ driverParam: boolean;
1256
+ notNull: true;
1257
+ hasDefault: true;
1258
+ isPrimaryKey: false;
1259
+ isAutoincrement: false;
1260
+ hasRuntimeDefault: false;
1261
+ enumValues: undefined;
1262
+ baseColumn: never;
1263
+ identity: undefined;
1264
+ generated: undefined;
1265
+ }, {}, {}>;
1266
+ createdAt: import("@porulle/core/drizzle").PgColumn<{
1267
+ name: "created_at";
1268
+ tableName: "pos_operator_pins";
1269
+ dataType: "date";
1270
+ columnType: "PgTimestamp";
1271
+ data: Date;
1272
+ driverParam: string;
1273
+ notNull: true;
1274
+ hasDefault: true;
1275
+ isPrimaryKey: false;
1276
+ isAutoincrement: false;
1277
+ hasRuntimeDefault: false;
1278
+ enumValues: undefined;
1279
+ baseColumn: never;
1280
+ identity: undefined;
1281
+ generated: undefined;
1282
+ }, {}, {}>;
1283
+ updatedAt: import("@porulle/core/drizzle").PgColumn<{
1284
+ name: "updated_at";
1285
+ tableName: "pos_operator_pins";
1286
+ dataType: "date";
1287
+ columnType: "PgTimestamp";
1288
+ data: Date;
1289
+ driverParam: string;
1290
+ notNull: true;
1291
+ hasDefault: true;
1292
+ isPrimaryKey: false;
1293
+ isAutoincrement: false;
1294
+ hasRuntimeDefault: false;
1295
+ enumValues: undefined;
1296
+ baseColumn: never;
1297
+ identity: undefined;
1298
+ generated: undefined;
1299
+ }, {}, {}>;
1300
+ };
1301
+ dialect: "pg";
1302
+ }>;
1303
+ export declare const posPinAttempts: import("drizzle-orm/pg-core/table").PgTableWithColumns<{
1304
+ name: "pos_pin_attempts";
1305
+ schema: undefined;
1306
+ columns: {
1307
+ id: import("@porulle/core/drizzle").PgColumn<{
1308
+ name: "id";
1309
+ tableName: "pos_pin_attempts";
1310
+ dataType: "string";
1311
+ columnType: "PgUUID";
1312
+ data: string;
1313
+ driverParam: string;
1314
+ notNull: true;
1315
+ hasDefault: true;
1316
+ isPrimaryKey: true;
1317
+ isAutoincrement: false;
1318
+ hasRuntimeDefault: false;
1319
+ enumValues: undefined;
1320
+ baseColumn: never;
1321
+ identity: undefined;
1322
+ generated: undefined;
1323
+ }, {}, {}>;
1324
+ organizationId: import("@porulle/core/drizzle").PgColumn<{
1325
+ name: "organization_id";
1326
+ tableName: "pos_pin_attempts";
1327
+ dataType: "string";
1328
+ columnType: "PgText";
1329
+ data: string;
1330
+ driverParam: string;
1331
+ notNull: true;
1332
+ hasDefault: false;
1333
+ isPrimaryKey: false;
1334
+ isAutoincrement: false;
1335
+ hasRuntimeDefault: false;
1336
+ enumValues: [string, ...string[]];
1337
+ baseColumn: never;
1338
+ identity: undefined;
1339
+ generated: undefined;
1340
+ }, {}, {}>;
1341
+ operatorId: import("@porulle/core/drizzle").PgColumn<{
1342
+ name: "operator_id";
1343
+ tableName: "pos_pin_attempts";
1344
+ dataType: "string";
1345
+ columnType: "PgText";
1346
+ data: string;
1347
+ driverParam: string;
1348
+ notNull: true;
1349
+ hasDefault: false;
1350
+ isPrimaryKey: false;
1351
+ isAutoincrement: false;
1352
+ hasRuntimeDefault: false;
1353
+ enumValues: [string, ...string[]];
1354
+ baseColumn: never;
1355
+ identity: undefined;
1356
+ generated: undefined;
1357
+ }, {}, {}>;
1358
+ failedCount: import("@porulle/core/drizzle").PgColumn<{
1359
+ name: "failed_count";
1360
+ tableName: "pos_pin_attempts";
1361
+ dataType: "number";
1362
+ columnType: "PgInteger";
1363
+ data: number;
1364
+ driverParam: string | number;
1365
+ notNull: true;
1366
+ hasDefault: true;
1367
+ isPrimaryKey: false;
1368
+ isAutoincrement: false;
1369
+ hasRuntimeDefault: false;
1370
+ enumValues: undefined;
1371
+ baseColumn: never;
1372
+ identity: undefined;
1373
+ generated: undefined;
1374
+ }, {}, {}>;
1375
+ lockedUntil: import("@porulle/core/drizzle").PgColumn<{
1376
+ name: "locked_until";
1377
+ tableName: "pos_pin_attempts";
1378
+ dataType: "date";
1379
+ columnType: "PgTimestamp";
1380
+ data: Date;
1381
+ driverParam: string;
1382
+ notNull: false;
1383
+ hasDefault: false;
1384
+ isPrimaryKey: false;
1385
+ isAutoincrement: false;
1386
+ hasRuntimeDefault: false;
1387
+ enumValues: undefined;
1388
+ baseColumn: never;
1389
+ identity: undefined;
1390
+ generated: undefined;
1391
+ }, {}, {}>;
1392
+ updatedAt: import("@porulle/core/drizzle").PgColumn<{
1393
+ name: "updated_at";
1394
+ tableName: "pos_pin_attempts";
1395
+ dataType: "date";
1396
+ columnType: "PgTimestamp";
1397
+ data: Date;
1398
+ driverParam: string;
1399
+ notNull: true;
1400
+ hasDefault: true;
1401
+ isPrimaryKey: false;
1402
+ isAutoincrement: false;
1403
+ hasRuntimeDefault: false;
1404
+ enumValues: undefined;
1405
+ baseColumn: never;
1406
+ identity: undefined;
1407
+ generated: undefined;
1408
+ }, {}, {}>;
1409
+ };
1410
+ dialect: "pg";
1411
+ }>;
1177
1412
  export declare const posReturnItems: import("drizzle-orm/pg-core/table").PgTableWithColumns<{
1178
1413
  name: "pos_return_items";
1179
1414
  schema: undefined;
@@ -1 +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"}
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,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYzB,CAAC;AAIJ,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASxB,CAAC;AAIJ,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAaxB,CAAC"}
package/dist/schema.js CHANGED
@@ -106,6 +106,31 @@ export const posPayments = pgTable("pos_payments", {
106
106
  }, (table) => ({
107
107
  transactionIdx: index("idx_pos_payments_transaction").on(table.transactionId),
108
108
  }));
109
+ // ─── Operator PINs (issue #51) ──────────────────────────────────────────
110
+ export const posOperatorPins = pgTable("pos_operator_pins", {
111
+ id: uuid("id").defaultRandom().primaryKey(),
112
+ organizationId: text("organization_id").notNull(),
113
+ operatorId: text("operator_id").notNull(),
114
+ // PBKDF2-SHA256, encoded as pbkdf2$<iterations>$<saltB64>$<hashB64>
115
+ pinHash: text("pin_hash").notNull(),
116
+ // Manager override capability — checked by POST /pos/auth/override.
117
+ canOverride: boolean("can_override").notNull().default(false),
118
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
119
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
120
+ }, (table) => ({
121
+ orgOperatorUnique: uniqueIndex("pos_operator_pins_org_operator_unique").on(table.organizationId, table.operatorId),
122
+ }));
123
+ // ─── PIN attempt lockout (SEC-15) ───────────────────────────────────────
124
+ export const posPinAttempts = pgTable("pos_pin_attempts", {
125
+ id: uuid("id").defaultRandom().primaryKey(),
126
+ organizationId: text("organization_id").notNull(),
127
+ operatorId: text("operator_id").notNull(),
128
+ failedCount: integer("failed_count").notNull().default(0),
129
+ lockedUntil: timestamp("locked_until", { withTimezone: true }),
130
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
131
+ }, (table) => ({
132
+ orgOperatorUnique: uniqueIndex("pos_pin_attempts_org_operator_unique").on(table.organizationId, table.operatorId),
133
+ }));
109
134
  // ─── Return Items ───────────────────────────────────────────────────────
110
135
  export const posReturnItems = pgTable("pos_return_items", {
111
136
  id: uuid("id").defaultRandom().primaryKey(),