@porulle/plugin-pos 0.7.0 → 0.8.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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -1
- package/dist/routes/auth.d.ts +19 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +60 -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/schema.d.ts +126 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +14 -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 +145 -0
- package/dist/services/pin-service.d.ts +62 -0
- package/dist/services/pin-service.d.ts.map +1 -0
- package/dist/services/pin-service.js +171 -0
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +5 -5
- package/src/index.ts +30 -0
- package/src/routes/auth.ts +67 -0
- package/src/routes/exchanges.ts +53 -0
- package/src/schema.ts +16 -0
- package/src/services/exchange-service.ts +227 -0
- package/src/services/pin-service.ts +228 -0
- package/src/types.ts +12 -0
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":"AA6BA,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,0CAsG3D"}
|
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, } 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,18 @@ export function posPlugin(userOptions = {}) {
|
|
|
69
75
|
posTransactions,
|
|
70
76
|
posPayments,
|
|
71
77
|
posReturnItems,
|
|
78
|
+
posOperatorPins,
|
|
79
|
+
}),
|
|
80
|
+
// Scope used by PIN login to mint per-shift credentials (issue #51).
|
|
81
|
+
apiKeyScopes: () => ({
|
|
82
|
+
[options.pinAuth.apiKeyScope ?? "pos"]: {
|
|
83
|
+
prefix: "pos_shift_",
|
|
84
|
+
description: "Short-lived per-shift POS credentials minted by PIN login.",
|
|
85
|
+
permissions: { pos: ["operate"] },
|
|
86
|
+
// Allow credentials as short as 15 minutes (Better Auth default
|
|
87
|
+
// minimum is 1 day — too long for per-shift keys).
|
|
88
|
+
keyExpiration: { minExpiresIn: 1 / 96 },
|
|
89
|
+
},
|
|
72
90
|
}),
|
|
73
91
|
hooks: () => [
|
|
74
92
|
buildPOSShippingHook(),
|
|
@@ -93,6 +111,8 @@ export function posPlugin(userOptions = {}) {
|
|
|
93
111
|
const returnService = new ReturnService(db);
|
|
94
112
|
const lookupService = new LookupService(db, ctx.services);
|
|
95
113
|
const receiptService = new ReceiptService(db, ctx.services);
|
|
114
|
+
const pinService = new PinService(db, options.pinAuth);
|
|
115
|
+
const exchangeService = new ExchangeService(db, ctx.services, transactionFn, transactionService, returnService);
|
|
96
116
|
// Cart service from core (for creating POS transaction carts)
|
|
97
117
|
const cartService = ctx.services.cart;
|
|
98
118
|
return [
|
|
@@ -103,6 +123,8 @@ export function posPlugin(userOptions = {}) {
|
|
|
103
123
|
...buildReturnRoutes(returnService, transactionService, paymentService, cartService, ctx),
|
|
104
124
|
...buildLookupRoutes(lookupService, ctx),
|
|
105
125
|
...buildReceiptRoutes(receiptService, ctx),
|
|
126
|
+
...buildPinAuthRoutes(pinService, ctx),
|
|
127
|
+
...buildExchangeRoutes(exchangeService, ctx),
|
|
106
128
|
];
|
|
107
129
|
},
|
|
108
130
|
});
|
|
@@ -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,KAAK,EAAE,UAAU,EAAc,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D;;;;;;;;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,60 @@
|
|
|
1
|
+
import { router } from "@porulle/core";
|
|
2
|
+
import { z } from "@hono/zod-openapi";
|
|
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 function buildPinAuthRoutes(service, ctx) {
|
|
13
|
+
const r = router("POS Auth", "/pos/auth", ctx);
|
|
14
|
+
r.put("/pin")
|
|
15
|
+
.summary("Set or rotate an operator PIN")
|
|
16
|
+
.permission("pos:admin")
|
|
17
|
+
.input(z.object({
|
|
18
|
+
operatorId: z.string().min(1),
|
|
19
|
+
pin: z.string().regex(/^\d{4,8}$/, "PIN must be 4-8 digits"),
|
|
20
|
+
canOverride: z.boolean().optional(),
|
|
21
|
+
}))
|
|
22
|
+
.handler(async ({ input, orgId }) => {
|
|
23
|
+
const body = input;
|
|
24
|
+
const result = await service.setPin(orgId, body);
|
|
25
|
+
if (!result.ok)
|
|
26
|
+
throw new Error(result.error);
|
|
27
|
+
return result.value;
|
|
28
|
+
});
|
|
29
|
+
r.post("/pin-login")
|
|
30
|
+
.summary("PIN login — mint a short-lived per-shift credential")
|
|
31
|
+
.permission("pos:operate")
|
|
32
|
+
.input(z.object({
|
|
33
|
+
operatorId: z.string().min(1),
|
|
34
|
+
pin: z.string().regex(/^\d{4,8}$/),
|
|
35
|
+
shiftId: z.string().uuid().optional(),
|
|
36
|
+
}))
|
|
37
|
+
.handler(async ({ input, orgId }) => {
|
|
38
|
+
const body = input;
|
|
39
|
+
const result = await service.pinLogin(orgId, body, ctx.auth);
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
throw new Error(result.error);
|
|
42
|
+
return result.value;
|
|
43
|
+
});
|
|
44
|
+
r.post("/override")
|
|
45
|
+
.summary("Manager override — approve one elevated action by PIN")
|
|
46
|
+
.permission("pos:operate")
|
|
47
|
+
.input(z.object({
|
|
48
|
+
operatorId: z.string().min(1),
|
|
49
|
+
pin: z.string().regex(/^\d{4,8}$/),
|
|
50
|
+
action: z.string().min(1).max(200),
|
|
51
|
+
}))
|
|
52
|
+
.handler(async ({ input, orgId }) => {
|
|
53
|
+
const body = input;
|
|
54
|
+
const result = await service.override(orgId, body);
|
|
55
|
+
if (!result.ok)
|
|
56
|
+
throw new Error(result.error);
|
|
57
|
+
return result.value;
|
|
58
|
+
});
|
|
59
|
+
return r.routes();
|
|
60
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1174,6 +1174,132 @@ 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
|
+
}>;
|
|
1177
1303
|
export declare const posReturnItems: import("drizzle-orm/pg-core/table").PgTableWithColumns<{
|
|
1178
1304
|
name: "pos_return_items";
|
|
1179
1305
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAaxB,CAAC"}
|
package/dist/schema.js
CHANGED
|
@@ -106,6 +106,20 @@ 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
|
+
}));
|
|
109
123
|
// ─── Return Items ───────────────────────────────────────────────────────
|
|
110
124
|
export const posReturnItems = pgTable("pos_return_items", {
|
|
111
125
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PluginResult } from "@porulle/core";
|
|
2
|
+
import type { Db, Transaction } from "../types.js";
|
|
3
|
+
import type { ReturnService } from "./return-service.js";
|
|
4
|
+
import type { TransactionService } from "./transaction-service.js";
|
|
5
|
+
/**
|
|
6
|
+
* Exchanges (issue #53): return line items + create a replacement order +
|
|
7
|
+
* settle the difference, in one call.
|
|
8
|
+
*
|
|
9
|
+
* The financially-critical half — the line-level refund on the original
|
|
10
|
+
* order (core issue #52 primitives: refundable-quantity enforcement, daily
|
|
11
|
+
* cap, audited ledger) and the replacement order creation — runs inside ONE
|
|
12
|
+
* database transaction, so a failure in either leaves no money moved and no
|
|
13
|
+
* order created. The POS bookkeeping rows (transaction, return items) are
|
|
14
|
+
* written after commit and cross-linked for audit.
|
|
15
|
+
*/
|
|
16
|
+
export interface ExchangeInput {
|
|
17
|
+
shiftId: string;
|
|
18
|
+
terminalId: string;
|
|
19
|
+
originalOrderId: string;
|
|
20
|
+
currency?: string | undefined;
|
|
21
|
+
customerId?: string | undefined;
|
|
22
|
+
returnItems: Array<{
|
|
23
|
+
originalLineItemId: string;
|
|
24
|
+
quantity: number;
|
|
25
|
+
reason: "defective" | "wrong_item" | "changed_mind" | "other";
|
|
26
|
+
}>;
|
|
27
|
+
replacementItems: Array<{
|
|
28
|
+
entityId: string;
|
|
29
|
+
variantId?: string | undefined;
|
|
30
|
+
sku?: string | undefined;
|
|
31
|
+
title: string;
|
|
32
|
+
quantity: number;
|
|
33
|
+
unitPrice: number;
|
|
34
|
+
taxAmount?: number | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export declare class ExchangeService {
|
|
38
|
+
private db;
|
|
39
|
+
private services;
|
|
40
|
+
private transaction;
|
|
41
|
+
private transactionService;
|
|
42
|
+
private returnService;
|
|
43
|
+
constructor(db: Db, services: Record<string, unknown>, transaction: <T>(fn: (tx: Db) => Promise<T>) => Promise<T>, transactionService: TransactionService, returnService: ReturnService);
|
|
44
|
+
exchange(orgId: string, input: ExchangeInput, actor: {
|
|
45
|
+
userId: string;
|
|
46
|
+
} & Record<string, unknown>): Promise<PluginResult<{
|
|
47
|
+
transaction: Transaction;
|
|
48
|
+
refundId: string;
|
|
49
|
+
returnTotal: number;
|
|
50
|
+
replacementOrderId: string;
|
|
51
|
+
replacementTotal: number;
|
|
52
|
+
netDelta: number;
|
|
53
|
+
}>>;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=exchange-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exchange-service.d.ts","sourceRoot":"","sources":["../../src/services/exchange-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,KAAK,CAAC;QACjB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,WAAW,GAAG,YAAY,GAAG,cAAc,GAAG,OAAO,CAAC;KAC/D,CAAC,CAAC;IACH,gBAAgB,EAAE,KAAK,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC/B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QACzB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAChC,CAAC,CAAC;CACJ;AAgBD,qBAAa,eAAe;IAExB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,kBAAkB;IAC1B,OAAO,CAAC,aAAa;gBAJb,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1D,kBAAkB,EAAE,kBAAkB,EACtC,aAAa,EAAE,aAAa;IAGhC,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClD,OAAO,CAAC,YAAY,CAAC;QACtB,WAAW,EAAE,WAAW,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;CAsJJ"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { eq } from "@porulle/core/drizzle";
|
|
2
|
+
import { Ok, Err } from "@porulle/core";
|
|
3
|
+
import { posTransactions } from "../schema.js";
|
|
4
|
+
export class ExchangeService {
|
|
5
|
+
db;
|
|
6
|
+
services;
|
|
7
|
+
transaction;
|
|
8
|
+
transactionService;
|
|
9
|
+
returnService;
|
|
10
|
+
constructor(db, services, transaction, transactionService, returnService) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.services = services;
|
|
13
|
+
this.transaction = transaction;
|
|
14
|
+
this.transactionService = transactionService;
|
|
15
|
+
this.returnService = returnService;
|
|
16
|
+
}
|
|
17
|
+
async exchange(orgId, input, actor) {
|
|
18
|
+
if (input.returnItems.length === 0)
|
|
19
|
+
return Err("At least one return item is required");
|
|
20
|
+
if (input.replacementItems.length === 0)
|
|
21
|
+
return Err("At least one replacement item is required");
|
|
22
|
+
const orders = this.services.orders;
|
|
23
|
+
const currency = input.currency ?? "USD";
|
|
24
|
+
const subtotal = input.replacementItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
25
|
+
const taxTotal = input.replacementItems.reduce((sum, i) => sum + (i.taxAmount ?? 0), 0);
|
|
26
|
+
const replacementTotal = subtotal + taxTotal;
|
|
27
|
+
// Atomic core half: refund the returned lines + create the replacement.
|
|
28
|
+
const core = await this.transaction(async (tx) => {
|
|
29
|
+
const txCtx = { tx, actor };
|
|
30
|
+
const refund = await orders.refundLines(input.originalOrderId, {
|
|
31
|
+
lines: input.returnItems.map((item) => ({
|
|
32
|
+
lineItemId: item.originalLineItemId,
|
|
33
|
+
quantity: item.quantity,
|
|
34
|
+
})),
|
|
35
|
+
reason: "exchange",
|
|
36
|
+
}, actor, txCtx);
|
|
37
|
+
if (!refund.ok || !refund.value) {
|
|
38
|
+
throw new Error(refund.error?.message ?? "Exchange refund failed");
|
|
39
|
+
}
|
|
40
|
+
const replacement = await orders.create({
|
|
41
|
+
currency,
|
|
42
|
+
subtotal,
|
|
43
|
+
taxTotal,
|
|
44
|
+
shippingTotal: 0,
|
|
45
|
+
grandTotal: replacementTotal,
|
|
46
|
+
...(input.customerId ? { customerId: input.customerId } : {}),
|
|
47
|
+
metadata: {
|
|
48
|
+
exchange: {
|
|
49
|
+
originalOrderId: input.originalOrderId,
|
|
50
|
+
refundId: refund.value.refund.id,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
lineItems: input.replacementItems.map((item) => ({
|
|
54
|
+
entityId: item.entityId,
|
|
55
|
+
entityType: "product",
|
|
56
|
+
...(item.variantId ? { variantId: item.variantId } : {}),
|
|
57
|
+
...(item.sku ? { sku: item.sku } : {}),
|
|
58
|
+
title: item.title,
|
|
59
|
+
quantity: item.quantity,
|
|
60
|
+
unitPrice: item.unitPrice,
|
|
61
|
+
totalPrice: item.unitPrice * item.quantity,
|
|
62
|
+
taxAmount: item.taxAmount ?? 0,
|
|
63
|
+
})),
|
|
64
|
+
}, actor, txCtx);
|
|
65
|
+
if (!replacement.ok || !replacement.value) {
|
|
66
|
+
throw new Error(replacement.error?.message ?? "Replacement order creation failed");
|
|
67
|
+
}
|
|
68
|
+
return { refund: refund.value.refund, replacementOrder: replacement.value };
|
|
69
|
+
}).catch((error) => error);
|
|
70
|
+
if (core instanceof Error)
|
|
71
|
+
return Err(core.message);
|
|
72
|
+
const returnTotal = core.refund.amount;
|
|
73
|
+
const netDelta = replacementTotal - returnTotal;
|
|
74
|
+
// POS bookkeeping (post-commit): cart + exchange transaction + return rows.
|
|
75
|
+
const cart = this.services.cart;
|
|
76
|
+
const cartResult = await cart.create({ currency, metadata: { posExchange: true, originalOrderId: input.originalOrderId } }, actor);
|
|
77
|
+
if (!cartResult.ok || !cartResult.value)
|
|
78
|
+
return Err("Failed to create cart for exchange");
|
|
79
|
+
const txnResult = await this.transactionService.create(orgId, {
|
|
80
|
+
shiftId: input.shiftId,
|
|
81
|
+
terminalId: input.terminalId,
|
|
82
|
+
operatorId: actor.userId,
|
|
83
|
+
cartId: cartResult.value.id,
|
|
84
|
+
type: "exchange",
|
|
85
|
+
...(input.customerId ? { customerId: input.customerId } : {}),
|
|
86
|
+
});
|
|
87
|
+
if (!txnResult.ok)
|
|
88
|
+
return Err(txnResult.error);
|
|
89
|
+
const txn = txnResult.value;
|
|
90
|
+
await this.returnService.addReturnItems(txn.id, input.returnItems.map((item) => ({
|
|
91
|
+
originalOrderId: input.originalOrderId,
|
|
92
|
+
originalLineItemId: item.originalLineItemId,
|
|
93
|
+
quantity: item.quantity,
|
|
94
|
+
reason: item.reason,
|
|
95
|
+
refundAmount: core.refund.lines.find((l) => l.lineItemId === item.originalLineItemId)?.amount ?? 0,
|
|
96
|
+
})));
|
|
97
|
+
await this.transactionService.updateTotals(txn.id, {
|
|
98
|
+
subtotal,
|
|
99
|
+
taxTotal,
|
|
100
|
+
total: netDelta,
|
|
101
|
+
discountTotal: 0,
|
|
102
|
+
});
|
|
103
|
+
// Cross-link everything on the POS transaction for audit.
|
|
104
|
+
await this.db
|
|
105
|
+
.update(posTransactions)
|
|
106
|
+
.set({
|
|
107
|
+
orderId: core.replacementOrder.id,
|
|
108
|
+
metadata: {
|
|
109
|
+
exchange: {
|
|
110
|
+
originalOrderId: input.originalOrderId,
|
|
111
|
+
refundId: core.refund.id,
|
|
112
|
+
replacementOrderId: core.replacementOrder.id,
|
|
113
|
+
returnTotal,
|
|
114
|
+
replacementTotal,
|
|
115
|
+
netDelta,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
updatedAt: new Date(),
|
|
119
|
+
})
|
|
120
|
+
.where(eq(posTransactions.id, txn.id));
|
|
121
|
+
// An even exchange settles itself — nothing to tender.
|
|
122
|
+
let finalTxn = txn;
|
|
123
|
+
if (netDelta === 0) {
|
|
124
|
+
const completed = await this.transactionService.complete(txn.id, core.replacementOrder.id);
|
|
125
|
+
if (completed.ok)
|
|
126
|
+
finalTxn = completed.value;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const rows = await this.db
|
|
130
|
+
.select()
|
|
131
|
+
.from(posTransactions)
|
|
132
|
+
.where(eq(posTransactions.id, txn.id));
|
|
133
|
+
if (rows[0])
|
|
134
|
+
finalTxn = rows[0];
|
|
135
|
+
}
|
|
136
|
+
return Ok({
|
|
137
|
+
transaction: finalTxn,
|
|
138
|
+
refundId: core.refund.id,
|
|
139
|
+
returnTotal,
|
|
140
|
+
replacementOrderId: core.replacementOrder.id,
|
|
141
|
+
replacementTotal,
|
|
142
|
+
netDelta,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|