@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.
- package/dist/hooks/checkout-pos.d.ts.map +1 -1
- package/dist/hooks/checkout-pos.js +9 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/routes/auth.d.ts +19 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +66 -0
- package/dist/routes/exchanges.d.ts +15 -0
- package/dist/routes/exchanges.d.ts.map +1 -0
- package/dist/routes/exchanges.js +42 -0
- package/dist/routes/returns.d.ts.map +1 -1
- package/dist/routes/returns.js +25 -4
- package/dist/schema.d.ts +235 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +25 -0
- package/dist/services/exchange-service.d.ts +55 -0
- package/dist/services/exchange-service.d.ts.map +1 -0
- package/dist/services/exchange-service.js +168 -0
- package/dist/services/pin-service.d.ts +73 -0
- package/dist/services/pin-service.d.ts.map +1 -0
- package/dist/services/pin-service.js +269 -0
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +5 -5
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +3 -3
- package/src/hooks/checkout-pos.ts +9 -2
- package/src/index.ts +33 -0
- package/src/routes/auth.ts +72 -0
- package/src/routes/exchanges.ts +53 -0
- package/src/routes/returns.ts +47 -5
- package/src/schema.ts +29 -0
- package/src/services/exchange-service.ts +272 -0
- package/src/services/pin-service.ts +350 -0
- package/src/services/transaction-service.ts +5 -4
- 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;
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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,
|
|
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"}
|
package/dist/routes/returns.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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;
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -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(),
|