@tallyui/connector-vendure 3.7.1 → 3.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/index.d.ts CHANGED
@@ -128,6 +128,186 @@ declare function searchVendureCustomers(context: SyncContext, query: string, opt
128
128
  declare function createVendureCustomer(context: SyncContext, input: CustomerInput): Promise<Customer>;
129
129
  declare function getVendureCustomer(context: SyncContext, id: string): Promise<Customer | null>;
130
130
 
131
+ interface VendureOrderListOptions {
132
+ /** Offset, default 0; negative or fractional values are truncated and floored to 0. */
133
+ skip?: number;
134
+ /** Page size, default 25, clamped to 1..100. */
135
+ take?: number;
136
+ /** Vendure's DateOperators on orderPlacedAt; only these three keys are sent. */
137
+ orderPlacedAt?: {
138
+ after?: string;
139
+ before?: string;
140
+ between?: {
141
+ start: string;
142
+ end: string;
143
+ };
144
+ };
145
+ /** Order states, sent as `state: { in }` when non-empty. */
146
+ state?: string[];
147
+ /** The plugin's register id custom field, sent as `tallyRegisterId: { eq }` when non-empty. */
148
+ tallyRegisterId?: string;
149
+ /** The plugin's cashier custom field, sent as `tallyCashierRef: { eq }` when non-empty. */
150
+ tallyCashierRef?: string;
151
+ /** Lists through `customer(id) { orders }` when non-empty. */
152
+ customerId?: string;
153
+ /** Trimmed; when non-empty, matches order code or customer last name with `contains`. */
154
+ search?: string;
155
+ /** Select the vendurepos plugin's custom fields; default true. False for a server without the plugin. */
156
+ tallyFields?: boolean;
157
+ }
158
+ interface VendureOrderList<T> {
159
+ items: T[];
160
+ totalItems: number;
161
+ }
162
+ interface VendureOrderSummary {
163
+ id: string;
164
+ code: string;
165
+ state: string;
166
+ orderPlacedAt: string | null;
167
+ updatedAt: string;
168
+ currencyCode: string;
169
+ totalQuantity: number;
170
+ total: number;
171
+ totalWithTax: number;
172
+ customer: {
173
+ id: string;
174
+ firstName: string;
175
+ lastName: string;
176
+ emailAddress: string;
177
+ } | null;
178
+ customFields?: {
179
+ tallyClientOrderId: string | null;
180
+ tallySaleAt: string | null;
181
+ tallyRegisterId: string | null;
182
+ tallySessionId: string | null;
183
+ tallyCashierRef: string | null;
184
+ tallyRejected: boolean | null;
185
+ tallyRejectedClientOrderId: string | null;
186
+ };
187
+ }
188
+ interface VendureOrder extends VendureOrderSummary {
189
+ active: boolean;
190
+ lines: {
191
+ id: string;
192
+ quantity: number;
193
+ orderPlacedQuantity: number;
194
+ proratedUnitPrice: number;
195
+ /** Vendure's per-unit value used in refund calculations, which the till sums for `order.refund`'s `totalMinor` (ADR-080). */
196
+ proratedUnitPriceWithTax: number;
197
+ taxRate: number;
198
+ productVariant: {
199
+ id: string;
200
+ name: string;
201
+ sku: string;
202
+ };
203
+ unitPrice: number;
204
+ unitPriceWithTax: number;
205
+ linePrice: number;
206
+ linePriceWithTax: number;
207
+ discountedLinePrice: number;
208
+ discountedLinePriceWithTax: number;
209
+ discounts: {
210
+ description: string;
211
+ amount: number;
212
+ amountWithTax: number;
213
+ }[];
214
+ customFields?: {
215
+ tallyCustomName: string | null;
216
+ tallyCustomSku: string | null;
217
+ tallyUnitPrice: number | null;
218
+ tallyPriceIncludesTax: boolean | null;
219
+ tallyClientLineId: string | null;
220
+ };
221
+ }[];
222
+ shippingLines: {
223
+ shippingMethod: {
224
+ name: string;
225
+ };
226
+ price: number;
227
+ priceWithTax: number;
228
+ }[];
229
+ surcharges: {
230
+ description: string;
231
+ sku: string;
232
+ price: number;
233
+ priceWithTax: number;
234
+ }[];
235
+ subTotal: number;
236
+ subTotalWithTax: number;
237
+ shipping: number;
238
+ shippingWithTax: number;
239
+ taxSummary: {
240
+ description: string;
241
+ taxRate: number;
242
+ taxBase: number;
243
+ taxTotal: number;
244
+ }[];
245
+ discounts: {
246
+ description: string;
247
+ amount: number;
248
+ amountWithTax: number;
249
+ }[];
250
+ couponCodes: string[];
251
+ payments?: {
252
+ id: string;
253
+ method: string;
254
+ amount: number;
255
+ state: string;
256
+ transactionId: string;
257
+ createdAt: string;
258
+ metadata: unknown;
259
+ refunds: {
260
+ id: string;
261
+ total: number;
262
+ items: number;
263
+ shipping: number;
264
+ adjustment: number;
265
+ state: string;
266
+ reason: string;
267
+ metadata: unknown;
268
+ lines: {
269
+ orderLineId: string;
270
+ quantity: number;
271
+ }[];
272
+ }[];
273
+ }[];
274
+ fulfillments?: {
275
+ id: string;
276
+ state: string;
277
+ method: string;
278
+ trackingCode: string;
279
+ }[];
280
+ customFields?: VendureOrderSummary['customFields'] & {
281
+ tallyPayments: string | null;
282
+ tallySnapshot: string | null;
283
+ tallyShipping: string | null;
284
+ };
285
+ }
286
+ declare function listVendureOrders(context: SyncContext, options?: VendureOrderListOptions): Promise<VendureOrderList<VendureOrderSummary>>;
287
+ declare function getVendureOrder(context: SyncContext, id: string, options?: {
288
+ tallyFields?: boolean;
289
+ }): Promise<VendureOrder | null>;
290
+
291
+ interface VendureRefundableLine {
292
+ orderLineId: string;
293
+ quantity: number;
294
+ refundedQuantity: number;
295
+ cancelledQuantity: number;
296
+ refundableQuantity: number;
297
+ unitRefundWithTax: number;
298
+ }
299
+ interface VendureRefundable {
300
+ lines: VendureRefundableLine[];
301
+ shippingWithTax: number;
302
+ moneyWithTax: number;
303
+ }
304
+ /**
305
+ * What the form may offer, from the order alone. Pending refunds count as refunded.
306
+ * The plugin is authoritative (`quantity_exceeds`, `amount_mismatch`).
307
+ * Amounts are Vendure's minor units with tax.
308
+ */
309
+ declare function vendureRefundable(order: Pick<VendureOrder, 'lines' | 'shippingWithTax' | 'payments'>): VendureRefundable;
310
+
131
311
  /**
132
312
  * Vendure connector for Tally UI. Build one per store session, anew on each sign-in or store change:
133
313
  * each instance owns its reconcile feed, whose queued work must never reach another store's database (#307).
@@ -165,4 +345,4 @@ declare const createVendureConnector: (options?: {
165
345
  */
166
346
  declare const vendureConnector: TallyConnector;
167
347
 
168
- export { type VendureCredentialKind, type VendureCredentials, VendureTimezoneConfigError, createVendureConnector, createVendureCustomer, createVendureProductReplication, createVendureVariantFeedReplication, getVendureCustomer, searchVendureCustomers, toVendureCustomer, vendureAuth, vendureAuthFieldSets, vendureConnector, vendureGlobalStockSettings, vendureProductReplication, vendureProductSchema, vendureProductSync, vendureProductTraits, vendureSignIn, vendureStockReconcile, vendureStoreSettings };
348
+ export { type VendureCredentialKind, type VendureCredentials, type VendureOrder, type VendureOrderList, type VendureOrderListOptions, type VendureOrderSummary, type VendureRefundable, type VendureRefundableLine, VendureTimezoneConfigError, createVendureConnector, createVendureCustomer, createVendureProductReplication, createVendureVariantFeedReplication, getVendureCustomer, getVendureOrder, listVendureOrders, searchVendureCustomers, toVendureCustomer, vendureAuth, vendureAuthFieldSets, vendureConnector, vendureGlobalStockSettings, vendureProductReplication, vendureProductSchema, vendureProductSync, vendureProductTraits, vendureRefundable, vendureSignIn, vendureStockReconcile, vendureStoreSettings };
package/dist/index.js CHANGED
@@ -966,6 +966,126 @@ async function vendureGlobalStockSettings(context) {
966
966
  return res.data.globalSettings;
967
967
  }
968
968
 
969
+ // src/orders.ts
970
+ var ORDER_ROW_FIELDS = `
971
+ id code state orderPlacedAt updatedAt currencyCode totalQuantity total totalWithTax
972
+ customer { id firstName lastName emailAddress }
973
+ `;
974
+ var ORDER_CUSTOM_FIELDS = "tallyClientOrderId tallySaleAt tallyRegisterId tallySessionId tallyCashierRef tallyRejected tallyRejectedClientOrderId";
975
+ var TALLY_ORDER_ROW_FIELDS = `${ORDER_ROW_FIELDS} customFields { ${ORDER_CUSTOM_FIELDS} }`;
976
+ var ORDER_DETAIL_FIELDS_START = `
977
+ id code state active orderPlacedAt updatedAt currencyCode totalQuantity
978
+ customer { id firstName lastName emailAddress }
979
+ lines {`;
980
+ var ORDER_DETAIL_FIELDS_END = `
981
+ id quantity orderPlacedQuantity proratedUnitPrice proratedUnitPriceWithTax taxRate productVariant { id name sku }
982
+ unitPrice unitPriceWithTax linePrice linePriceWithTax discountedLinePrice discountedLinePriceWithTax
983
+ discounts { description amount amountWithTax }
984
+ }
985
+ shippingLines { shippingMethod { name } price priceWithTax }
986
+ surcharges { description sku price priceWithTax }
987
+ subTotal subTotalWithTax shipping shippingWithTax total totalWithTax
988
+ taxSummary { description taxRate taxBase taxTotal }
989
+ discounts { description amount amountWithTax }
990
+ couponCodes
991
+ payments { id method amount state transactionId createdAt metadata refunds { id total items shipping adjustment state reason metadata lines { orderLineId quantity } } }
992
+ fulfillments { id state method trackingCode }
993
+ `;
994
+ var ORDER_DETAIL_FIELDS = `${ORDER_DETAIL_FIELDS_START}${ORDER_DETAIL_FIELDS_END}`;
995
+ var TALLY_ORDER_DETAIL_FIELDS = `${ORDER_DETAIL_FIELDS_START}
996
+ customFields { tallyCustomName tallyCustomSku tallyUnitPrice tallyPriceIncludesTax tallyClientLineId }
997
+ ${ORDER_DETAIL_FIELDS_END} customFields { ${ORDER_CUSTOM_FIELDS} tallyPayments tallySnapshot tallyShipping }`;
998
+ var LIST_QUERY = `query Orders($options: OrderListOptions) {
999
+ orders(options: $options) { items { ${ORDER_ROW_FIELDS} } totalItems }
1000
+ }`;
1001
+ var TALLY_LIST_QUERY = LIST_QUERY.replace(ORDER_ROW_FIELDS, TALLY_ORDER_ROW_FIELDS);
1002
+ var CUSTOMER_LIST_QUERY = `query CustomerOrders($customerId: ID!, $options: OrderListOptions) {
1003
+ customer(id: $customerId) { orders(options: $options) { items { ${ORDER_ROW_FIELDS} } totalItems } }
1004
+ }`;
1005
+ var TALLY_CUSTOMER_LIST_QUERY = CUSTOMER_LIST_QUERY.replace(ORDER_ROW_FIELDS, TALLY_ORDER_ROW_FIELDS);
1006
+ var DETAIL_QUERY = `query Order($id: ID!) {
1007
+ order(id: $id) { ${ORDER_DETAIL_FIELDS} }
1008
+ }`;
1009
+ var TALLY_DETAIL_QUERY = `query Order($id: ID!) {
1010
+ order(id: $id) { ${TALLY_ORDER_DETAIL_FIELDS} }
1011
+ }`;
1012
+ async function listVendureOrders(context, options = {}) {
1013
+ const skip = Math.max(0, Math.trunc(Number.isFinite(options.skip) ? options.skip : 0));
1014
+ const take = Math.max(1, Math.min(100, Math.trunc(Number.isFinite(options.take) ? options.take : 25)));
1015
+ const date = options.orderPlacedAt;
1016
+ const orderPlacedAt = {
1017
+ ...date?.after !== void 0 ? { after: date.after } : {},
1018
+ ...date?.before !== void 0 ? { before: date.before } : {},
1019
+ ...date?.between !== void 0 ? { between: date.between } : {}
1020
+ };
1021
+ const q = options.search?.trim();
1022
+ const filter = {
1023
+ orderPlacedAt: Object.keys(orderPlacedAt).length ? orderPlacedAt : { isNull: false },
1024
+ ...options.state?.length ? { state: { in: options.state } } : {},
1025
+ ...options.tallyRegisterId ? { tallyRegisterId: { eq: options.tallyRegisterId } } : {},
1026
+ ...options.tallyCashierRef ? { tallyCashierRef: { eq: options.tallyCashierRef } } : {},
1027
+ ...q ? { _or: [{ code: { contains: q } }, { customerLastName: { contains: q } }] } : {}
1028
+ };
1029
+ const variables = {
1030
+ options: { skip, take, sort: { orderPlacedAt: "DESC", id: "DESC" }, filter },
1031
+ ...options.customerId ? { customerId: options.customerId } : {}
1032
+ };
1033
+ const query2 = options.customerId ? options.tallyFields === false ? CUSTOMER_LIST_QUERY : TALLY_CUSTOMER_LIST_QUERY : options.tallyFields === false ? LIST_QUERY : TALLY_LIST_QUERY;
1034
+ const body = await gql2(context, query2, variables);
1035
+ if (options.customerId && body?.data?.customer === null) return { items: [], totalItems: 0 };
1036
+ const result = options.customerId ? body?.data?.customer?.orders : body?.data?.orders;
1037
+ if (!result || typeof result !== "object" || !Array.isArray(result.items) || typeof result.totalItems !== "number") {
1038
+ throw new Error("Vendure sent an unexpected orders response");
1039
+ }
1040
+ return { items: result.items, totalItems: result.totalItems };
1041
+ }
1042
+ async function getVendureOrder(context, id, options) {
1043
+ const body = await gql2(context, options?.tallyFields === false ? DETAIL_QUERY : TALLY_DETAIL_QUERY, { id });
1044
+ const order = body?.data?.order;
1045
+ if (order === null) return null;
1046
+ if (typeof order !== "object" || typeof order?.id !== "string") {
1047
+ throw new Error("Vendure sent an unexpected order response");
1048
+ }
1049
+ return order;
1050
+ }
1051
+
1052
+ // src/refunds.ts
1053
+ function vendureRefundable(order) {
1054
+ const refundedQuantities = /* @__PURE__ */ new Map();
1055
+ let refundedShipping = 0;
1056
+ let moneyWithTax = 0;
1057
+ for (const payment of order.payments ?? []) {
1058
+ let refundedTotal = 0;
1059
+ for (const refund of payment.refunds ?? []) {
1060
+ if (refund.state === "Failed") continue;
1061
+ refundedTotal += Number.isFinite(refund.total) ? refund.total : 0;
1062
+ refundedShipping += Number.isFinite(refund.shipping) ? refund.shipping : 0;
1063
+ for (const line of refund.lines ?? []) {
1064
+ refundedQuantities.set(line.orderLineId, (refundedQuantities.get(line.orderLineId) ?? 0) + (Number.isFinite(line.quantity) ? line.quantity : 0));
1065
+ }
1066
+ }
1067
+ if (payment.state === "Settled") {
1068
+ moneyWithTax += Math.max(0, (Number.isFinite(payment.amount) ? payment.amount : 0) - refundedTotal);
1069
+ }
1070
+ }
1071
+ return {
1072
+ lines: (order.lines ?? []).map((line) => {
1073
+ const quantity = Number.isFinite(line.quantity) ? line.quantity : 0;
1074
+ const refundedQuantity = refundedQuantities.get(line.id) ?? 0;
1075
+ return {
1076
+ orderLineId: line.id,
1077
+ quantity,
1078
+ refundedQuantity,
1079
+ cancelledQuantity: Number.isFinite(line.orderPlacedQuantity) ? Math.max(0, line.orderPlacedQuantity - quantity) : 0,
1080
+ refundableQuantity: Math.max(0, quantity - refundedQuantity),
1081
+ unitRefundWithTax: Number.isFinite(line.proratedUnitPriceWithTax) ? line.proratedUnitPriceWithTax : 0
1082
+ };
1083
+ }),
1084
+ shippingWithTax: Math.max(0, (Number.isFinite(order.shippingWithTax) ? order.shippingWithTax : 0) - refundedShipping),
1085
+ moneyWithTax
1086
+ };
1087
+ }
1088
+
969
1089
  // src/index.ts
970
1090
  import { ConnectorUnauthorizedError as ConnectorUnauthorizedError6 } from "@tallyui/core";
971
1091
  var VENDURE_IDS_PER_REQUEST = 1e3;
@@ -1024,6 +1144,8 @@ export {
1024
1144
  createVendureProductReplication,
1025
1145
  createVendureVariantFeedReplication,
1026
1146
  getVendureCustomer,
1147
+ getVendureOrder,
1148
+ listVendureOrders,
1027
1149
  searchVendureCustomers,
1028
1150
  toVendureCustomer,
1029
1151
  vendureAuth,
@@ -1034,6 +1156,7 @@ export {
1034
1156
  vendureProductSchema,
1035
1157
  vendureProductSync,
1036
1158
  vendureProductTraits,
1159
+ vendureRefundable,
1037
1160
  vendureSignIn,
1038
1161
  vendureStockReconcile,
1039
1162
  vendureStoreSettings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/connector-vendure",
3
- "version": "3.7.1",
3
+ "version": "3.9.0",
4
4
  "type": "module",
5
5
  "description": "Vendure connector for Tally UI",
6
6
  "main": "./dist/index.js",
@@ -10,7 +10,8 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js",
13
- "source": "./src/index.ts"
13
+ "source": "./src/index.ts",
14
+ "default": "./dist/index.js"
14
15
  }
15
16
  },
16
17
  "files": [
@@ -26,11 +27,11 @@
26
27
  "directory": "connectors/vendure"
27
28
  },
28
29
  "peerDependencies": {
29
- "@tallyui/core": "^3.7.1"
30
+ "@tallyui/core": "^3.9.0"
30
31
  },
31
32
  "devDependencies": {
32
- "@tallyui/database": "3.7.1",
33
- "@tallyui/pos": "3.7.1",
33
+ "@tallyui/database": "3.9.0",
34
+ "@tallyui/pos": "3.9.0",
34
35
  "rxdb": "17.5.0"
35
36
  },
36
37
  "scripts": {
package/src/index.ts CHANGED
@@ -114,6 +114,8 @@ export { vendureStockReconcile } from './reconcile/stock';
114
114
  export { vendureStoreSettings } from './store-settings';
115
115
  export { vendureGlobalStockSettings } from './global-settings';
116
116
  export { searchVendureCustomers, createVendureCustomer, getVendureCustomer, toVendureCustomer } from './customers';
117
+ export { listVendureOrders, getVendureOrder, type VendureOrderListOptions, type VendureOrderList, type VendureOrderSummary, type VendureOrder } from './orders';
118
+ export { vendureRefundable, type VendureRefundableLine, type VendureRefundable } from './refunds';
117
119
 
118
120
  export { ConnectorUnauthorizedError } from '@tallyui/core';
119
121
  export { VendureTimezoneConfigError } from './replication/products';
package/src/orders.ts ADDED
@@ -0,0 +1,187 @@
1
+ import type { SyncContext } from '@tallyui/core';
2
+ import { gql } from './replication/products';
3
+
4
+ export interface VendureOrderListOptions {
5
+ /** Offset, default 0; negative or fractional values are truncated and floored to 0. */
6
+ skip?: number;
7
+ /** Page size, default 25, clamped to 1..100. */
8
+ take?: number;
9
+ /** Vendure's DateOperators on orderPlacedAt; only these three keys are sent. */
10
+ orderPlacedAt?: { after?: string; before?: string; between?: { start: string; end: string } };
11
+ /** Order states, sent as `state: { in }` when non-empty. */
12
+ state?: string[];
13
+ /** The plugin's register id custom field, sent as `tallyRegisterId: { eq }` when non-empty. */
14
+ tallyRegisterId?: string;
15
+ /** The plugin's cashier custom field, sent as `tallyCashierRef: { eq }` when non-empty. */
16
+ tallyCashierRef?: string;
17
+ /** Lists through `customer(id) { orders }` when non-empty. */
18
+ customerId?: string;
19
+ /** Trimmed; when non-empty, matches order code or customer last name with `contains`. */
20
+ search?: string;
21
+ /** Select the vendurepos plugin's custom fields; default true. False for a server without the plugin. */
22
+ tallyFields?: boolean;
23
+ }
24
+
25
+ export interface VendureOrderList<T> { items: T[]; totalItems: number }
26
+
27
+ export interface VendureOrderSummary {
28
+ id: string;
29
+ code: string;
30
+ state: string;
31
+ orderPlacedAt: string | null;
32
+ updatedAt: string;
33
+ currencyCode: string;
34
+ totalQuantity: number;
35
+ total: number;
36
+ totalWithTax: number;
37
+ customer: { id: string; firstName: string; lastName: string; emailAddress: string } | null;
38
+ customFields?: {
39
+ tallyClientOrderId: string | null;
40
+ tallySaleAt: string | null;
41
+ tallyRegisterId: string | null;
42
+ tallySessionId: string | null;
43
+ tallyCashierRef: string | null;
44
+ tallyRejected: boolean | null;
45
+ tallyRejectedClientOrderId: string | null;
46
+ };
47
+ }
48
+
49
+ export interface VendureOrder extends VendureOrderSummary {
50
+ active: boolean;
51
+ lines: {
52
+ id: string;
53
+ quantity: number;
54
+ orderPlacedQuantity: number;
55
+ proratedUnitPrice: number;
56
+ /** Vendure's per-unit value used in refund calculations, which the till sums for `order.refund`'s `totalMinor` (ADR-080). */
57
+ proratedUnitPriceWithTax: number;
58
+ taxRate: number;
59
+ productVariant: { id: string; name: string; sku: string };
60
+ unitPrice: number;
61
+ unitPriceWithTax: number;
62
+ linePrice: number;
63
+ linePriceWithTax: number;
64
+ discountedLinePrice: number;
65
+ discountedLinePriceWithTax: number;
66
+ discounts: { description: string; amount: number; amountWithTax: number }[];
67
+ customFields?: {
68
+ tallyCustomName: string | null;
69
+ tallyCustomSku: string | null;
70
+ tallyUnitPrice: number | null;
71
+ tallyPriceIncludesTax: boolean | null;
72
+ tallyClientLineId: string | null;
73
+ };
74
+ }[];
75
+ shippingLines: { shippingMethod: { name: string }; price: number; priceWithTax: number }[];
76
+ surcharges: { description: string; sku: string; price: number; priceWithTax: number }[];
77
+ subTotal: number;
78
+ subTotalWithTax: number;
79
+ shipping: number;
80
+ shippingWithTax: number;
81
+ taxSummary: { description: string; taxRate: number; taxBase: number; taxTotal: number }[];
82
+ discounts: { description: string; amount: number; amountWithTax: number }[];
83
+ couponCodes: string[];
84
+ payments?: {
85
+ id: string;
86
+ method: string;
87
+ amount: number;
88
+ state: string;
89
+ transactionId: string;
90
+ createdAt: string;
91
+ metadata: unknown;
92
+ refunds: { id: string; total: number; items: number; shipping: number; adjustment: number; state: string; reason: string; metadata: unknown; lines: { orderLineId: string; quantity: number }[] }[];
93
+ }[];
94
+ fulfillments?: { id: string; state: string; method: string; trackingCode: string }[];
95
+ customFields?: VendureOrderSummary['customFields'] & {
96
+ tallyPayments: string | null;
97
+ tallySnapshot: string | null;
98
+ tallyShipping: string | null;
99
+ };
100
+ }
101
+
102
+ const ORDER_ROW_FIELDS = `
103
+ id code state orderPlacedAt updatedAt currencyCode totalQuantity total totalWithTax
104
+ customer { id firstName lastName emailAddress }
105
+ `;
106
+ const ORDER_CUSTOM_FIELDS = 'tallyClientOrderId tallySaleAt tallyRegisterId tallySessionId tallyCashierRef tallyRejected tallyRejectedClientOrderId';
107
+ const TALLY_ORDER_ROW_FIELDS = `${ORDER_ROW_FIELDS} customFields { ${ORDER_CUSTOM_FIELDS} }`;
108
+ const ORDER_DETAIL_FIELDS_START = `
109
+ id code state active orderPlacedAt updatedAt currencyCode totalQuantity
110
+ customer { id firstName lastName emailAddress }
111
+ lines {`;
112
+ const ORDER_DETAIL_FIELDS_END = `
113
+ id quantity orderPlacedQuantity proratedUnitPrice proratedUnitPriceWithTax taxRate productVariant { id name sku }
114
+ unitPrice unitPriceWithTax linePrice linePriceWithTax discountedLinePrice discountedLinePriceWithTax
115
+ discounts { description amount amountWithTax }
116
+ }
117
+ shippingLines { shippingMethod { name } price priceWithTax }
118
+ surcharges { description sku price priceWithTax }
119
+ subTotal subTotalWithTax shipping shippingWithTax total totalWithTax
120
+ taxSummary { description taxRate taxBase taxTotal }
121
+ discounts { description amount amountWithTax }
122
+ couponCodes
123
+ payments { id method amount state transactionId createdAt metadata refunds { id total items shipping adjustment state reason metadata lines { orderLineId quantity } } }
124
+ fulfillments { id state method trackingCode }
125
+ `;
126
+ const ORDER_DETAIL_FIELDS = `${ORDER_DETAIL_FIELDS_START}${ORDER_DETAIL_FIELDS_END}`;
127
+ const TALLY_ORDER_DETAIL_FIELDS = `${ORDER_DETAIL_FIELDS_START}
128
+ customFields { tallyCustomName tallyCustomSku tallyUnitPrice tallyPriceIncludesTax tallyClientLineId }
129
+ ${ORDER_DETAIL_FIELDS_END} customFields { ${ORDER_CUSTOM_FIELDS} tallyPayments tallySnapshot tallyShipping }`;
130
+
131
+ const LIST_QUERY = `query Orders($options: OrderListOptions) {
132
+ orders(options: $options) { items { ${ORDER_ROW_FIELDS} } totalItems }
133
+ }`;
134
+ const TALLY_LIST_QUERY = LIST_QUERY.replace(ORDER_ROW_FIELDS, TALLY_ORDER_ROW_FIELDS);
135
+ const CUSTOMER_LIST_QUERY = `query CustomerOrders($customerId: ID!, $options: OrderListOptions) {
136
+ customer(id: $customerId) { orders(options: $options) { items { ${ORDER_ROW_FIELDS} } totalItems } }
137
+ }`;
138
+ const TALLY_CUSTOMER_LIST_QUERY = CUSTOMER_LIST_QUERY.replace(ORDER_ROW_FIELDS, TALLY_ORDER_ROW_FIELDS);
139
+ const DETAIL_QUERY = `query Order($id: ID!) {
140
+ order(id: $id) { ${ORDER_DETAIL_FIELDS} }
141
+ }`;
142
+ const TALLY_DETAIL_QUERY = `query Order($id: ID!) {
143
+ order(id: $id) { ${TALLY_ORDER_DETAIL_FIELDS} }
144
+ }`;
145
+
146
+ export async function listVendureOrders(context: SyncContext, options: VendureOrderListOptions = {}): Promise<VendureOrderList<VendureOrderSummary>> {
147
+ const skip = Math.max(0, Math.trunc(Number.isFinite(options.skip) ? options.skip! : 0));
148
+ const take = Math.max(1, Math.min(100, Math.trunc(Number.isFinite(options.take) ? options.take! : 25)));
149
+ const date = options.orderPlacedAt;
150
+ const orderPlacedAt = {
151
+ ...(date?.after !== undefined ? { after: date.after } : {}),
152
+ ...(date?.before !== undefined ? { before: date.before } : {}),
153
+ ...(date?.between !== undefined ? { between: date.between } : {}),
154
+ };
155
+ const q = options.search?.trim();
156
+ const filter = {
157
+ orderPlacedAt: Object.keys(orderPlacedAt).length ? orderPlacedAt : { isNull: false },
158
+ ...(options.state?.length ? { state: { in: options.state } } : {}),
159
+ ...(options.tallyRegisterId ? { tallyRegisterId: { eq: options.tallyRegisterId } } : {}),
160
+ ...(options.tallyCashierRef ? { tallyCashierRef: { eq: options.tallyCashierRef } } : {}),
161
+ ...(q ? { _or: [{ code: { contains: q } }, { customerLastName: { contains: q } }] } : {}),
162
+ };
163
+ const variables = {
164
+ options: { skip, take, sort: { orderPlacedAt: 'DESC', id: 'DESC' }, filter },
165
+ ...(options.customerId ? { customerId: options.customerId } : {}),
166
+ };
167
+ const query = options.customerId
168
+ ? (options.tallyFields === false ? CUSTOMER_LIST_QUERY : TALLY_CUSTOMER_LIST_QUERY)
169
+ : (options.tallyFields === false ? LIST_QUERY : TALLY_LIST_QUERY);
170
+ const body = await gql(context, query, variables);
171
+ if (options.customerId && body?.data?.customer === null) return { items: [], totalItems: 0 };
172
+ const result = options.customerId ? body?.data?.customer?.orders : body?.data?.orders;
173
+ if (!result || typeof result !== 'object' || !Array.isArray(result.items) || typeof result.totalItems !== 'number') {
174
+ throw new Error('Vendure sent an unexpected orders response');
175
+ }
176
+ return { items: result.items, totalItems: result.totalItems };
177
+ }
178
+
179
+ export async function getVendureOrder(context: SyncContext, id: string, options?: { tallyFields?: boolean }): Promise<VendureOrder | null> {
180
+ const body = await gql(context, options?.tallyFields === false ? DETAIL_QUERY : TALLY_DETAIL_QUERY, { id });
181
+ const order = body?.data?.order;
182
+ if (order === null) return null;
183
+ if (typeof order !== 'object' || typeof order?.id !== 'string') {
184
+ throw new Error('Vendure sent an unexpected order response');
185
+ }
186
+ return order;
187
+ }
package/src/refunds.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { VendureOrder } from './orders';
2
+
3
+ export interface VendureRefundableLine {
4
+ orderLineId: string;
5
+ quantity: number; // The line's current quantity.
6
+ refundedQuantity: number; // Sum of its refund lines over refunds not Failed.
7
+ cancelledQuantity: number; // max(0, orderPlacedQuantity - quantity).
8
+ refundableQuantity: number; // max(0, quantity - refundedQuantity).
9
+ unitRefundWithTax: number; // The line's proratedUnitPriceWithTax.
10
+ }
11
+
12
+ export interface VendureRefundable {
13
+ lines: VendureRefundableLine[]; // In the order's line order, one per line.
14
+ shippingWithTax: number; // max(0, order.shippingWithTax - shipping refunded over refunds not Failed).
15
+ moneyWithTax: number; // Sum over Settled payments of max(0, amount - refunds' total not Failed).
16
+ }
17
+
18
+ /**
19
+ * What the form may offer, from the order alone. Pending refunds count as refunded.
20
+ * The plugin is authoritative (`quantity_exceeds`, `amount_mismatch`).
21
+ * Amounts are Vendure's minor units with tax.
22
+ */
23
+ export function vendureRefundable(order: Pick<VendureOrder, 'lines' | 'shippingWithTax' | 'payments'>): VendureRefundable {
24
+ const refundedQuantities = new Map<string, number>();
25
+ let refundedShipping = 0;
26
+ let moneyWithTax = 0;
27
+ for (const payment of order.payments ?? []) {
28
+ let refundedTotal = 0;
29
+ for (const refund of payment.refunds ?? []) {
30
+ if (refund.state === 'Failed') continue;
31
+ refundedTotal += Number.isFinite(refund.total) ? refund.total : 0;
32
+ refundedShipping += Number.isFinite(refund.shipping) ? refund.shipping : 0;
33
+ for (const line of refund.lines ?? []) {
34
+ refundedQuantities.set(line.orderLineId, (refundedQuantities.get(line.orderLineId) ?? 0)
35
+ + (Number.isFinite(line.quantity) ? line.quantity : 0));
36
+ }
37
+ }
38
+ if (payment.state === 'Settled') {
39
+ moneyWithTax += Math.max(0, (Number.isFinite(payment.amount) ? payment.amount : 0) - refundedTotal);
40
+ }
41
+ }
42
+ return {
43
+ lines: (order.lines ?? []).map(line => {
44
+ const quantity = Number.isFinite(line.quantity) ? line.quantity : 0;
45
+ const refundedQuantity = refundedQuantities.get(line.id) ?? 0;
46
+ return {
47
+ orderLineId: line.id,
48
+ quantity,
49
+ refundedQuantity,
50
+ cancelledQuantity: Number.isFinite(line.orderPlacedQuantity) ? Math.max(0, line.orderPlacedQuantity - quantity) : 0,
51
+ refundableQuantity: Math.max(0, quantity - refundedQuantity),
52
+ unitRefundWithTax: Number.isFinite(line.proratedUnitPriceWithTax) ? line.proratedUnitPriceWithTax : 0,
53
+ };
54
+ }),
55
+ shippingWithTax: Math.max(0, (Number.isFinite(order.shippingWithTax) ? order.shippingWithTax : 0) - refundedShipping),
56
+ moneyWithTax,
57
+ };
58
+ }