@tallyui/connector-woocommerce 3.8.0 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/index.d.ts +28 -2
- package/dist/index.js +235 -4
- package/package.json +5 -4
- package/src/index.ts +9 -0
- package/src/reconcile/coupons.ts +85 -0
- package/src/replication/coupons.ts +16 -0
- package/src/schemas/coupons.ts +122 -0
- package/src/store-settings.ts +28 -0
package/README.md
CHANGED
|
@@ -35,6 +35,18 @@ The incremental pull can miss an edit: a `modified_after` that over-excludes (in
|
|
|
35
35
|
- When `wcpos/v2/status` lists `products_id_fast_path` in `capabilities`, the whole catalogue is listed in one request (`per_page=-1` with `_fields=id,date_modified_gmt,stock_quantity,stock_status`) instead of pages of 100; a store that refuses it (a 400) is listed page by page in the same pass.
|
|
36
36
|
- Both listings key on the numeric product id, never the till-local uuid; the adapter's `matchKey` is how the runner finds the till's copy.
|
|
37
37
|
|
|
38
|
+
## Capabilities
|
|
39
|
+
|
|
40
|
+
`readWooCapabilities` (the connector's `capabilities`) runs when the till signs in or restores a session. The app keeps the result with the session.
|
|
41
|
+
- **`GET wcpos/v2/stores`** gives the tax rounding (`tax_round_at_subtotal`). A failed or malformed read returns `undefined`: nothing new is known, and the stored capabilities stay.
|
|
42
|
+
- **`GET wcpos/v2/status`.** The strings in its `capabilities` decide two fields:
|
|
43
|
+
- `order_create_v5` sets `orderCreate` to 5; without it, `orderCreate` is 3. Only the TallyUI fork of the plugin advertises it, and the site version never decides it.
|
|
44
|
+
- `order_payments_list` sets `multiplePayments`.
|
|
45
|
+
- **`GET wcpos/v2/site`.** Its `wcpos_version` decides `coupons` (ADR-077, ruling R3). It is true only for woocommerce-pos 1.9.0 or later; a 1.9.0 pre-release does not count. Any failed or unclear read gives false, and the till then refuses to apply a coupon.
|
|
46
|
+
- **`lineTax`** is always `{ none: true, classes: true }`.
|
|
47
|
+
- **Unauthorised reads.** A 401 or 403 from `/stores` or `/status` throws `ConnectorUnauthorizedError`. A 401 or 403 from `/site` only closes the coupon gate.
|
|
48
|
+
- **`products_id_fast_path`** is read by the reconcile pass, not here (see Reconciliation).
|
|
49
|
+
|
|
38
50
|
## Customers
|
|
39
51
|
|
|
40
52
|
On WCPOS 1.10.x, `searchCustomers` uses `GET wcpos/v2/customers` with `role=customer`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SyncContext, ServerCapabilities, StoreSettings, Customer, CustomerInput, OrderCreateEnvelope, CommandResult,
|
|
1
|
+
import { SyncContext, ServerCapabilities, StoreSettings, Customer, CustomerInput, OrderCreateEnvelope, CommandResult, ReconcileFeed, CatalogueReconcileAdapter, ProductTraits, CollectionSync, ReplicationAdapter, ConnectorUnauthorizedError, TallyConnector } from '@tallyui/core';
|
|
2
2
|
export { ConnectorUnauthorizedError } from '@tallyui/core';
|
|
3
3
|
import { RxJsonSchema } from 'rxdb';
|
|
4
4
|
|
|
@@ -96,6 +96,32 @@ declare function createWooCommandTransport(options: WooCommandTransportOptions):
|
|
|
96
96
|
*/
|
|
97
97
|
declare const wooProductSchema: RxJsonSchema<any>;
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* WooCommerce Coupon RxDB schema (ADR-077 amendment 2).
|
|
101
|
+
* Mirrors the coupon listing; the catalogue reconcile is its only source.
|
|
102
|
+
*/
|
|
103
|
+
declare const wooCouponSchema: RxJsonSchema<any>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One coupon row as the till stores it: a coupon without a uuid cannot be keyed and is skipped, never thrown,
|
|
107
|
+
* so one bad row cannot stall the coupons' pull. A coupon that is not published arrives deleted.
|
|
108
|
+
*/
|
|
109
|
+
declare function toCouponDocument(coupon: any): any | undefined;
|
|
110
|
+
|
|
111
|
+
/** Coupon usage never moves the modified time (ADR-077 amendment 2); numeric used_by ids match stored strings. */
|
|
112
|
+
declare const wooCouponFingerprint: (c: {
|
|
113
|
+
date_modified_gmt?: string | null;
|
|
114
|
+
usage_count?: number | null;
|
|
115
|
+
used_by?: readonly unknown[] | null;
|
|
116
|
+
}) => string;
|
|
117
|
+
/** Coupons reach the collection only through this feed, keyed by uuid and matched to new listing entries by id. */
|
|
118
|
+
declare function createWooCouponFeed(): ReconcileFeed<any>;
|
|
119
|
+
/**
|
|
120
|
+
* The coupons' only source (ADR-077 amendment 2): page through published coupons, with no date filter,
|
|
121
|
+
* and hand differences to the feed. confirmGone proves deletion by re-reading the published listing by id.
|
|
122
|
+
*/
|
|
123
|
+
declare function wooCouponReconcile(feed: Pick<ReconcileFeed<any>, 'enqueue'>): CatalogueReconcileAdapter<any, number>;
|
|
124
|
+
|
|
99
125
|
/**
|
|
100
126
|
* WooCommerce product trait implementations.
|
|
101
127
|
*
|
|
@@ -226,4 +252,4 @@ declare function createWooCommerceConnector(): TallyConnector;
|
|
|
226
252
|
*/
|
|
227
253
|
declare const woocommerceConnector: TallyConnector;
|
|
228
254
|
|
|
229
|
-
export { WOO_ORDER_CREATE_VERSION, type WooCommandTransportOptions, WooDateFilterError, WooMissingTokenError, WooMissingUuidError, WooTillUpdateRequiredError, WooTokenRefusedError, createWooCommandTransport, createWooCommerceConnector, createWooCustomers, readWooCapabilities, toWooCustomer, toWooOrderPayload, wcposClientPart, wooCatalogueReconcile, wooProductReplication, wooProductSchema, wooProductSync, wooProductTraits, wooReconcileFingerprint, wooStoreSettings, woocommerceConnector };
|
|
255
|
+
export { WOO_ORDER_CREATE_VERSION, type WooCommandTransportOptions, WooDateFilterError, WooMissingTokenError, WooMissingUuidError, WooTillUpdateRequiredError, WooTokenRefusedError, createWooCommandTransport, createWooCommerceConnector, createWooCouponFeed, createWooCustomers, readWooCapabilities, toCouponDocument, toWooCustomer, toWooOrderPayload, wcposClientPart, wooCatalogueReconcile, wooCouponFingerprint, wooCouponReconcile, wooCouponSchema, wooProductReplication, wooProductSchema, wooProductSync, wooProductTraits, wooReconcileFingerprint, wooStoreSettings, woocommerceConnector };
|
package/dist/index.js
CHANGED
|
@@ -170,6 +170,124 @@ var wooProductSchema = {
|
|
|
170
170
|
required: ["uuid"]
|
|
171
171
|
};
|
|
172
172
|
|
|
173
|
+
// src/schemas/coupons.ts
|
|
174
|
+
var wooCouponSchema = {
|
|
175
|
+
title: "WooCommerce Coupon",
|
|
176
|
+
version: 0,
|
|
177
|
+
type: "object",
|
|
178
|
+
primaryKey: "uuid",
|
|
179
|
+
properties: {
|
|
180
|
+
uuid: {
|
|
181
|
+
type: "string",
|
|
182
|
+
maxLength: 36
|
|
183
|
+
},
|
|
184
|
+
id: {
|
|
185
|
+
type: "integer",
|
|
186
|
+
multipleOf: 1,
|
|
187
|
+
minimum: 0,
|
|
188
|
+
maximum: 2147483647
|
|
189
|
+
},
|
|
190
|
+
code: {
|
|
191
|
+
type: "string",
|
|
192
|
+
maxLength: 255
|
|
193
|
+
},
|
|
194
|
+
status: {
|
|
195
|
+
type: "string"
|
|
196
|
+
},
|
|
197
|
+
amount: {
|
|
198
|
+
type: "string"
|
|
199
|
+
},
|
|
200
|
+
discount_type: {
|
|
201
|
+
type: "string"
|
|
202
|
+
},
|
|
203
|
+
description: {
|
|
204
|
+
type: "string"
|
|
205
|
+
},
|
|
206
|
+
date_created: {
|
|
207
|
+
type: "string"
|
|
208
|
+
},
|
|
209
|
+
date_created_gmt: {
|
|
210
|
+
type: "string"
|
|
211
|
+
},
|
|
212
|
+
date_modified: {
|
|
213
|
+
type: "string"
|
|
214
|
+
},
|
|
215
|
+
date_modified_gmt: {
|
|
216
|
+
type: "string"
|
|
217
|
+
},
|
|
218
|
+
minimum_amount: {
|
|
219
|
+
type: "string"
|
|
220
|
+
},
|
|
221
|
+
maximum_amount: {
|
|
222
|
+
type: "string"
|
|
223
|
+
},
|
|
224
|
+
date_expires: {
|
|
225
|
+
type: ["string", "null"]
|
|
226
|
+
},
|
|
227
|
+
date_expires_gmt: {
|
|
228
|
+
type: ["string", "null"]
|
|
229
|
+
},
|
|
230
|
+
usage_count: {
|
|
231
|
+
type: "integer"
|
|
232
|
+
},
|
|
233
|
+
usage_limit: {
|
|
234
|
+
type: ["integer", "null"]
|
|
235
|
+
},
|
|
236
|
+
usage_limit_per_user: {
|
|
237
|
+
type: ["integer", "null"]
|
|
238
|
+
},
|
|
239
|
+
limit_usage_to_x_items: {
|
|
240
|
+
type: ["integer", "null"]
|
|
241
|
+
},
|
|
242
|
+
individual_use: {
|
|
243
|
+
type: "boolean"
|
|
244
|
+
},
|
|
245
|
+
free_shipping: {
|
|
246
|
+
type: "boolean"
|
|
247
|
+
},
|
|
248
|
+
exclude_sale_items: {
|
|
249
|
+
type: "boolean"
|
|
250
|
+
},
|
|
251
|
+
product_ids: {
|
|
252
|
+
type: "array",
|
|
253
|
+
items: { type: "integer" }
|
|
254
|
+
},
|
|
255
|
+
excluded_product_ids: {
|
|
256
|
+
type: "array",
|
|
257
|
+
items: { type: "integer" }
|
|
258
|
+
},
|
|
259
|
+
product_categories: {
|
|
260
|
+
type: "array",
|
|
261
|
+
items: { type: "integer" }
|
|
262
|
+
},
|
|
263
|
+
excluded_product_categories: {
|
|
264
|
+
type: "array",
|
|
265
|
+
items: { type: "integer" }
|
|
266
|
+
},
|
|
267
|
+
email_restrictions: {
|
|
268
|
+
type: "array",
|
|
269
|
+
items: { type: "string" }
|
|
270
|
+
},
|
|
271
|
+
used_by: {
|
|
272
|
+
type: "array",
|
|
273
|
+
items: { type: "string" }
|
|
274
|
+
},
|
|
275
|
+
meta_data: {
|
|
276
|
+
type: "array",
|
|
277
|
+
items: {
|
|
278
|
+
type: "object",
|
|
279
|
+
properties: {
|
|
280
|
+
id: { type: "integer" },
|
|
281
|
+
key: { type: "string" },
|
|
282
|
+
value: {}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
indexes: ["code"],
|
|
288
|
+
required: ["uuid"]
|
|
289
|
+
};
|
|
290
|
+
|
|
173
291
|
// src/traits/product.ts
|
|
174
292
|
import { moneyFromMajor } from "@tallyui/core";
|
|
175
293
|
|
|
@@ -631,6 +749,82 @@ function createWooReconcileFeed() {
|
|
|
631
749
|
return createReconcileFeed({ key: (doc) => doc.uuid, remote: (doc) => doc.id, fetchByIds: wooFetchByIds });
|
|
632
750
|
}
|
|
633
751
|
|
|
752
|
+
// src/reconcile/coupons.ts
|
|
753
|
+
import { createReconcileFeed as createReconcileFeed2 } from "@tallyui/core";
|
|
754
|
+
|
|
755
|
+
// src/replication/coupons.ts
|
|
756
|
+
function toCouponDocument(coupon) {
|
|
757
|
+
const uuid = wooProductUuid(coupon);
|
|
758
|
+
if (uuid === void 0) return void 0;
|
|
759
|
+
const projected = Object.fromEntries(Object.keys(wooCouponSchema.properties).filter((key) => Object.prototype.hasOwnProperty.call(coupon, key)).map((key) => [key, coupon[key]]));
|
|
760
|
+
if (Array.isArray(projected.used_by)) projected.used_by = projected.used_by.map(String);
|
|
761
|
+
return { ...projected, uuid, _deleted: coupon.status !== "publish" };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/reconcile/coupons.ts
|
|
765
|
+
var PAGE_SIZE2 = 100;
|
|
766
|
+
var LISTING_FIELDS2 = "id,date_modified_gmt,usage_count,used_by";
|
|
767
|
+
var wooCouponFingerprint = (c) => [c.date_modified_gmt ?? "", c.usage_count ?? "", JSON.stringify((c.used_by ?? []).map(String))].join("|");
|
|
768
|
+
async function get2(path, context) {
|
|
769
|
+
const response = await fetch(`${context.baseUrl}${path}`, {
|
|
770
|
+
headers: { ...context.headers, "Content-Type": "application/json" },
|
|
771
|
+
signal: context.signal
|
|
772
|
+
});
|
|
773
|
+
await checkResponse(response);
|
|
774
|
+
return response.json();
|
|
775
|
+
}
|
|
776
|
+
async function wooFetchCouponsByIds(entries, context) {
|
|
777
|
+
const ids = entries.map((e) => e.local?.id ?? e.remote).filter((id) => Number.isInteger(id));
|
|
778
|
+
const documents = [];
|
|
779
|
+
for (let i = 0; i < ids.length; i += MAX_IDS_PER_REQUEST) {
|
|
780
|
+
const chunk = ids.slice(i, i + MAX_IDS_PER_REQUEST);
|
|
781
|
+
const params = new URLSearchParams({ include: chunk.join(","), per_page: String(MAX_IDS_PER_REQUEST), status: "any" });
|
|
782
|
+
const rows = await get2(`/coupons?${params}`, context);
|
|
783
|
+
documents.push(...rows.map(toCouponDocument).filter((doc) => doc !== void 0));
|
|
784
|
+
}
|
|
785
|
+
return documents;
|
|
786
|
+
}
|
|
787
|
+
function createWooCouponFeed() {
|
|
788
|
+
return createReconcileFeed2({ key: (doc) => doc.uuid, remote: (doc) => doc.id, fetchByIds: wooFetchCouponsByIds });
|
|
789
|
+
}
|
|
790
|
+
var toEntries2 = (rows) => rows.filter((row) => Number.isInteger(row?.id)).map((row) => ({ key: String(row.id), fingerprint: wooCouponFingerprint(row), remote: row.id }));
|
|
791
|
+
function wooCouponReconcile(feed) {
|
|
792
|
+
return {
|
|
793
|
+
async *fetchPages(context, from = 1) {
|
|
794
|
+
for (let page = from; ; page++) {
|
|
795
|
+
const params = new URLSearchParams({
|
|
796
|
+
per_page: String(PAGE_SIZE2),
|
|
797
|
+
page: String(page),
|
|
798
|
+
orderby: "id",
|
|
799
|
+
order: "asc",
|
|
800
|
+
status: "publish",
|
|
801
|
+
_fields: LISTING_FIELDS2
|
|
802
|
+
});
|
|
803
|
+
const rows = await get2(`/coupons?${params}`, context);
|
|
804
|
+
yield { entries: toEntries2(rows), cursor: page + 1 };
|
|
805
|
+
if (rows.length < PAGE_SIZE2) return;
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
fingerprint: wooCouponFingerprint,
|
|
809
|
+
// The listing keys on the numeric id, never the till-local uuid.
|
|
810
|
+
matchKey: (doc) => Number.isInteger(doc.id) ? String(doc.id) : void 0,
|
|
811
|
+
async confirmGone(locals, context) {
|
|
812
|
+
const ids = locals.map((doc) => doc.id).filter((id) => Number.isInteger(id));
|
|
813
|
+
if (!ids.length) return [];
|
|
814
|
+
const live = /* @__PURE__ */ new Map();
|
|
815
|
+
for (let i = 0; i < ids.length; i += MAX_IDS_PER_REQUEST) {
|
|
816
|
+
const chunk = ids.slice(i, i + MAX_IDS_PER_REQUEST);
|
|
817
|
+
const params = new URLSearchParams({ include: chunk.join(","), per_page: String(MAX_IDS_PER_REQUEST), _fields: "id,meta_data" });
|
|
818
|
+
const rows = await get2(`/coupons?${params}`, context);
|
|
819
|
+
for (const row of rows) live.set(row.id, wooProductUuid(row));
|
|
820
|
+
}
|
|
821
|
+
return locals.filter((doc) => Number.isInteger(doc.id) && (!live.has(doc.id) || typeof live.get(doc.id) === "string" && live.get(doc.id) !== doc.uuid)).map((doc) => doc.uuid);
|
|
822
|
+
},
|
|
823
|
+
// Without an integer id, the feed would find nothing and tombstone without proof; proven tombstones pass.
|
|
824
|
+
enqueue: (entries) => feed.enqueue(entries.filter((e) => e.tombstone || Number.isInteger(e.local?.id) || Number.isInteger(e.remote)))
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
634
828
|
// src/customers.ts
|
|
635
829
|
import { CustomerServiceError } from "@tallyui/core";
|
|
636
830
|
function toWooCustomer(raw) {
|
|
@@ -736,6 +930,18 @@ function createWooCustomers(options) {
|
|
|
736
930
|
import { ConnectorUnauthorizedError as ConnectorUnauthorizedError2, StoreSettingsError, minorUnitDigits, woocommerceTax } from "@tallyui/core";
|
|
737
931
|
var WOO_ORDER_CREATE_VERSION = 5;
|
|
738
932
|
var WOO_BASE_ORDER_CREATE_VERSION = 3;
|
|
933
|
+
var WOO_COUPONS_MIN_PLUGIN = [1, 9, 0];
|
|
934
|
+
function wooPluginSupportsCoupons(version2) {
|
|
935
|
+
if (typeof version2 !== "string") return false;
|
|
936
|
+
const match = /^(\d+)\.(\d+)(?:\.(\d+))?(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/.exec(version2.trim());
|
|
937
|
+
if (!match) return false;
|
|
938
|
+
const parts = [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
|
|
939
|
+
for (let index = 0; index < parts.length; index++) {
|
|
940
|
+
if (parts[index] > WOO_COUPONS_MIN_PLUGIN[index]) return true;
|
|
941
|
+
if (parts[index] < WOO_COUPONS_MIN_PLUGIN[index]) return false;
|
|
942
|
+
}
|
|
943
|
+
return !match[4];
|
|
944
|
+
}
|
|
739
945
|
async function wooStoreSettings(context) {
|
|
740
946
|
try {
|
|
741
947
|
const init = { method: "GET", headers: context.headers, signal: context.signal };
|
|
@@ -765,6 +971,7 @@ async function wooStoreSettings(context) {
|
|
|
765
971
|
currency,
|
|
766
972
|
pricesIncludeTax: store.prices_include_tax === "yes",
|
|
767
973
|
taxRoundAtSubtotal: store.tax_round_at_subtotal === "yes",
|
|
974
|
+
calcDiscountsSequentially: (store.woocommerce_calc_discounts_sequentially ?? store.calc_discounts_sequentially) === "yes",
|
|
768
975
|
...typeof store.shipping_tax_class === "string" ? { shippingTaxClass: store.shipping_tax_class } : {},
|
|
769
976
|
taxClassSlugs: classes.map((row) => row.slug),
|
|
770
977
|
taxRates,
|
|
@@ -846,11 +1053,26 @@ async function readWooCapabilities(context) {
|
|
|
846
1053
|
} catch (error) {
|
|
847
1054
|
if (error instanceof ConnectorUnauthorizedError2) throw error;
|
|
848
1055
|
}
|
|
1056
|
+
let coupons = false;
|
|
1057
|
+
try {
|
|
1058
|
+
const site = await fetch(`${context.baseUrl}/site`, {
|
|
1059
|
+
method: "GET",
|
|
1060
|
+
headers: context.headers,
|
|
1061
|
+
signal: context.signal
|
|
1062
|
+
});
|
|
1063
|
+
if (site.ok) {
|
|
1064
|
+
const body = await site.json();
|
|
1065
|
+
coupons = wooPluginSupportsCoupons(body?.wcpos_version);
|
|
1066
|
+
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
coupons = false;
|
|
1069
|
+
}
|
|
849
1070
|
const orderCreate = capabilities.includes("order_create_v5") ? WOO_ORDER_CREATE_VERSION : WOO_BASE_ORDER_CREATE_VERSION;
|
|
850
1071
|
return {
|
|
851
1072
|
orderCreate,
|
|
852
1073
|
taxRounding: { granularity: "woocommerce", roundAtSubtotal: stores[0].tax_round_at_subtotal === "yes" },
|
|
853
1074
|
multiplePayments,
|
|
1075
|
+
coupons,
|
|
854
1076
|
// WCPOS v5 takes a fee's, shipping line's and custom line's tax_status and tax_class (orders #146, #161); it has no /tally/v1/info.
|
|
855
1077
|
lineTax: { none: true, classes: true }
|
|
856
1078
|
};
|
|
@@ -861,7 +1083,7 @@ async function readWooCapabilities(context) {
|
|
|
861
1083
|
}
|
|
862
1084
|
|
|
863
1085
|
// package.json
|
|
864
|
-
var version = "3.
|
|
1086
|
+
var version = "3.9.1";
|
|
865
1087
|
|
|
866
1088
|
// src/commands/transport.ts
|
|
867
1089
|
import { minorUnitDigits as minorUnitDigits2, moneyFromDecimalString, moneyFromMajor as moneyFromMajor2 } from "@tallyui/core";
|
|
@@ -1069,6 +1291,7 @@ var WooMissingTokenError = class extends ConnectorUnauthorizedError3 {
|
|
|
1069
1291
|
};
|
|
1070
1292
|
function createWooCommerceConnector() {
|
|
1071
1293
|
const catalogueFeed = createWooReconcileFeed();
|
|
1294
|
+
const couponFeed = createWooCouponFeed();
|
|
1072
1295
|
const customers = createWooCustomers();
|
|
1073
1296
|
return {
|
|
1074
1297
|
...wooConnectorParts,
|
|
@@ -1078,11 +1301,13 @@ function createWooCommerceConnector() {
|
|
|
1078
1301
|
// One replication per collection; the reconcile feed is last, so its fetch wins duplicates;
|
|
1079
1302
|
// duplicates are matched by uuid, the primary key, not the store id (#331). legacyKey
|
|
1080
1303
|
// reads an existing install's plain pull checkpoint as the product feed's, so it does not resync.
|
|
1081
|
-
products: combinePullAdapters({ products: wooProductReplication, reconcile: catalogueFeed.adapter }, { legacyKey: "products", key: (doc) => doc.uuid })
|
|
1304
|
+
products: combinePullAdapters({ products: wooProductReplication, reconcile: catalogueFeed.adapter }, { legacyKey: "products", key: (doc) => doc.uuid }),
|
|
1305
|
+
coupons: couponFeed.adapter
|
|
1082
1306
|
},
|
|
1083
1307
|
reconcile: {
|
|
1084
1308
|
// The feed's fetchByIds asks for at most this many ids per request; the runner budgets by requests.
|
|
1085
|
-
catalogue: { ...wooCatalogueReconcile(catalogueFeed), refetchBatchSize: MAX_IDS_PER_REQUEST }
|
|
1309
|
+
catalogue: { ...wooCatalogueReconcile(catalogueFeed), refetchBatchSize: MAX_IDS_PER_REQUEST },
|
|
1310
|
+
coupons: { ...wooCouponReconcile(couponFeed), refetchBatchSize: MAX_IDS_PER_REQUEST }
|
|
1086
1311
|
}
|
|
1087
1312
|
};
|
|
1088
1313
|
}
|
|
@@ -1126,7 +1351,8 @@ var wooConnectorParts = {
|
|
|
1126
1351
|
}
|
|
1127
1352
|
},
|
|
1128
1353
|
schemas: {
|
|
1129
|
-
products: wooProductSchema
|
|
1354
|
+
products: wooProductSchema,
|
|
1355
|
+
coupons: wooCouponSchema
|
|
1130
1356
|
},
|
|
1131
1357
|
traits: {
|
|
1132
1358
|
product: wooProductTraits
|
|
@@ -1146,12 +1372,17 @@ export {
|
|
|
1146
1372
|
WooTokenRefusedError,
|
|
1147
1373
|
createWooCommandTransport,
|
|
1148
1374
|
createWooCommerceConnector,
|
|
1375
|
+
createWooCouponFeed,
|
|
1149
1376
|
createWooCustomers,
|
|
1150
1377
|
readWooCapabilities,
|
|
1378
|
+
toCouponDocument,
|
|
1151
1379
|
toWooCustomer,
|
|
1152
1380
|
toWooOrderPayload,
|
|
1153
1381
|
wcposClientPart,
|
|
1154
1382
|
wooCatalogueReconcile,
|
|
1383
|
+
wooCouponFingerprint,
|
|
1384
|
+
wooCouponReconcile,
|
|
1385
|
+
wooCouponSchema,
|
|
1155
1386
|
wooProductReplication,
|
|
1156
1387
|
wooProductSchema,
|
|
1157
1388
|
wooProductSync,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyui/connector-woocommerce",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WooCommerce 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,10 +27,10 @@
|
|
|
26
27
|
"directory": "connectors/woocommerce"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
|
-
"@tallyui/core": "^3.
|
|
30
|
+
"@tallyui/core": "^3.9.1"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
|
-
"@tallyui/database": "3.
|
|
33
|
+
"@tallyui/database": "3.9.1",
|
|
33
34
|
"rxdb": "17.5.0"
|
|
34
35
|
},
|
|
35
36
|
"scripts": {
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { combinePullAdapters, ConnectorUnauthorizedError, type TallyConnector } from '@tallyui/core';
|
|
2
2
|
|
|
3
3
|
import { wooProductSchema } from './schemas/products';
|
|
4
|
+
import { wooCouponSchema } from './schemas/coupons';
|
|
4
5
|
import { wooProductTraits } from './traits/product';
|
|
5
6
|
import { wooProductSync } from './sync/products';
|
|
6
7
|
import { wooProductReplication } from './replication/products';
|
|
7
8
|
import { wooCatalogueReconcile } from './reconcile/catalogue';
|
|
8
9
|
import { createWooReconcileFeed, MAX_IDS_PER_REQUEST } from './reconcile/feed';
|
|
10
|
+
import { createWooCouponFeed, wooCouponReconcile } from './reconcile/coupons';
|
|
9
11
|
import { createWooCustomers } from './customers';
|
|
10
12
|
import { wooStoreSettings, readWooCapabilities } from './store-settings';
|
|
11
13
|
import { version } from '../package.json';
|
|
@@ -43,6 +45,7 @@ export class WooMissingTokenError extends ConnectorUnauthorizedError {
|
|
|
43
45
|
export function createWooCommerceConnector(): TallyConnector {
|
|
44
46
|
// The catalogue reconcile's corrections reach `products` only through this pull adapter (#248).
|
|
45
47
|
const catalogueFeed = createWooReconcileFeed();
|
|
48
|
+
const couponFeed = createWooCouponFeed();
|
|
46
49
|
const customers = createWooCustomers();
|
|
47
50
|
return {
|
|
48
51
|
...wooConnectorParts,
|
|
@@ -53,10 +56,12 @@ export function createWooCommerceConnector(): TallyConnector {
|
|
|
53
56
|
// duplicates are matched by uuid, the primary key, not the store id (#331). legacyKey
|
|
54
57
|
// reads an existing install's plain pull checkpoint as the product feed's, so it does not resync.
|
|
55
58
|
products: combinePullAdapters({ products: wooProductReplication, reconcile: catalogueFeed.adapter }, { legacyKey: 'products', key: (doc: any) => doc.uuid }),
|
|
59
|
+
coupons: couponFeed.adapter,
|
|
56
60
|
},
|
|
57
61
|
reconcile: {
|
|
58
62
|
// The feed's fetchByIds asks for at most this many ids per request; the runner budgets by requests.
|
|
59
63
|
catalogue: { ...wooCatalogueReconcile(catalogueFeed), refetchBatchSize: MAX_IDS_PER_REQUEST },
|
|
64
|
+
coupons: { ...wooCouponReconcile(couponFeed), refetchBatchSize: MAX_IDS_PER_REQUEST },
|
|
60
65
|
},
|
|
61
66
|
};
|
|
62
67
|
}
|
|
@@ -104,6 +109,7 @@ const wooConnectorParts = {
|
|
|
104
109
|
|
|
105
110
|
schemas: {
|
|
106
111
|
products: wooProductSchema,
|
|
112
|
+
coupons: wooCouponSchema,
|
|
107
113
|
},
|
|
108
114
|
|
|
109
115
|
traits: {
|
|
@@ -127,6 +133,9 @@ export { createWooCustomers, toWooCustomer } from './customers';
|
|
|
127
133
|
export { createWooCommandTransport, toWooOrderPayload, type WooCommandTransportOptions } from './commands/transport';
|
|
128
134
|
export { ConnectorUnauthorizedError } from '@tallyui/core';
|
|
129
135
|
export { wooProductSchema } from './schemas/products';
|
|
136
|
+
export { wooCouponSchema } from './schemas/coupons';
|
|
137
|
+
export { toCouponDocument } from './replication/coupons';
|
|
138
|
+
export { wooCouponReconcile, wooCouponFingerprint, createWooCouponFeed } from './reconcile/coupons';
|
|
130
139
|
export { wooProductTraits } from './traits/product';
|
|
131
140
|
export { wooProductSync } from './sync/products';
|
|
132
141
|
export { wooProductReplication, WooDateFilterError, WooTokenRefusedError, WooMissingUuidError, WooTillUpdateRequiredError } from './replication/products';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createReconcileFeed, type CatalogueReconcileAdapter, type CatalogueReconcileEntry, type ReconcileFeed, type ReconcileFetchEntry, type SyncContext } from '@tallyui/core';
|
|
2
|
+
import { toCouponDocument } from '../replication/coupons';
|
|
3
|
+
import { checkResponse, wooProductUuid } from '../replication/products';
|
|
4
|
+
import { MAX_IDS_PER_REQUEST } from './feed';
|
|
5
|
+
|
|
6
|
+
/** The listing's page size, WordPress's per_page maximum. */
|
|
7
|
+
const PAGE_SIZE = 100;
|
|
8
|
+
/** The numeric id for refetching, and the fingerprint's fields. */
|
|
9
|
+
const LISTING_FIELDS = 'id,date_modified_gmt,usage_count,used_by';
|
|
10
|
+
|
|
11
|
+
/** Coupon usage never moves the modified time (ADR-077 amendment 2); numeric used_by ids match stored strings. */
|
|
12
|
+
export const wooCouponFingerprint = (c: { date_modified_gmt?: string | null; usage_count?: number | null; used_by?: readonly unknown[] | null }): string =>
|
|
13
|
+
[c.date_modified_gmt ?? '', c.usage_count ?? '', JSON.stringify((c.used_by ?? []).map(String))].join('|');
|
|
14
|
+
|
|
15
|
+
async function get(path: string, context: SyncContext): Promise<any> {
|
|
16
|
+
const response = await fetch(`${context.baseUrl}${path}`, {
|
|
17
|
+
headers: { ...context.headers, 'Content-Type': 'application/json' },
|
|
18
|
+
signal: context.signal,
|
|
19
|
+
});
|
|
20
|
+
await checkResponse(response);
|
|
21
|
+
return response.json();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Full coupons by numeric id (the till's own id, else the listing's remote). A draft arrives deleted;
|
|
26
|
+
* a trashed coupon is absent from status=any, so the feed tombstones its local copy.
|
|
27
|
+
*/
|
|
28
|
+
export async function wooFetchCouponsByIds(entries: Array<ReconcileFetchEntry<any>>, context: SyncContext): Promise<any[]> {
|
|
29
|
+
const ids = entries.map((e) => e.local?.id ?? e.remote).filter((id): id is number => Number.isInteger(id));
|
|
30
|
+
const documents: any[] = [];
|
|
31
|
+
for (let i = 0; i < ids.length; i += MAX_IDS_PER_REQUEST) {
|
|
32
|
+
const chunk = ids.slice(i, i + MAX_IDS_PER_REQUEST);
|
|
33
|
+
const params = new URLSearchParams({ include: chunk.join(','), per_page: String(MAX_IDS_PER_REQUEST), status: 'any' });
|
|
34
|
+
const rows: any[] = await get(`/coupons?${params}`, context);
|
|
35
|
+
documents.push(...rows.map(toCouponDocument).filter((doc) => doc !== undefined));
|
|
36
|
+
}
|
|
37
|
+
return documents;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Coupons reach the collection only through this feed, keyed by uuid and matched to new listing entries by id. */
|
|
41
|
+
export function createWooCouponFeed(): ReconcileFeed<any> {
|
|
42
|
+
return createReconcileFeed<any>({ key: (doc) => doc.uuid, remote: (doc) => doc.id, fetchByIds: wooFetchCouponsByIds });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Listing rows keyed by numeric id; a row without an integer id cannot be asked about, and is dropped. */
|
|
46
|
+
const toEntries = (rows: any[]) => rows.filter((row) => Number.isInteger(row?.id))
|
|
47
|
+
.map((row): CatalogueReconcileEntry => ({ key: String(row.id), fingerprint: wooCouponFingerprint(row), remote: row.id }));
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The coupons' only source (ADR-077 amendment 2): page through published coupons, with no date filter,
|
|
51
|
+
* and hand differences to the feed. confirmGone proves deletion by re-reading the published listing by id.
|
|
52
|
+
*/
|
|
53
|
+
export function wooCouponReconcile(feed: Pick<ReconcileFeed<any>, 'enqueue'>): CatalogueReconcileAdapter<any, number> {
|
|
54
|
+
return {
|
|
55
|
+
async *fetchPages(context, from = 1) {
|
|
56
|
+
for (let page = from; ; page++) {
|
|
57
|
+
const params = new URLSearchParams({
|
|
58
|
+
per_page: String(PAGE_SIZE), page: String(page), orderby: 'id', order: 'asc', status: 'publish', _fields: LISTING_FIELDS,
|
|
59
|
+
});
|
|
60
|
+
const rows: any[] = await get(`/coupons?${params}`, context);
|
|
61
|
+
yield { entries: toEntries(rows), cursor: page + 1 };
|
|
62
|
+
if (rows.length < PAGE_SIZE) return;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
fingerprint: wooCouponFingerprint,
|
|
66
|
+
// The listing keys on the numeric id, never the till-local uuid.
|
|
67
|
+
matchKey: (doc) => (Number.isInteger(doc.id) ? String(doc.id) : undefined),
|
|
68
|
+
async confirmGone(locals, context) {
|
|
69
|
+
const ids = locals.map((doc) => doc.id).filter((id) => Number.isInteger(id));
|
|
70
|
+
// An empty include= would list the whole store; a local without a numeric id has no proof and is kept.
|
|
71
|
+
if (!ids.length) return [];
|
|
72
|
+
const live = new Map<number, string | undefined>();
|
|
73
|
+
for (let i = 0; i < ids.length; i += MAX_IDS_PER_REQUEST) {
|
|
74
|
+
const chunk = ids.slice(i, i + MAX_IDS_PER_REQUEST);
|
|
75
|
+
const params = new URLSearchParams({ include: chunk.join(','), per_page: String(MAX_IDS_PER_REQUEST), _fields: 'id,meta_data' });
|
|
76
|
+
const rows: any[] = await get(`/coupons?${params}`, context);
|
|
77
|
+
for (const row of rows) live.set(row.id, wooProductUuid(row));
|
|
78
|
+
}
|
|
79
|
+
// A live id backs only the store's uuid (#369); a live row without a uuid keeps the local copy.
|
|
80
|
+
return locals.filter((doc) => Number.isInteger(doc.id) && (!live.has(doc.id) || (typeof live.get(doc.id) === 'string' && live.get(doc.id) !== doc.uuid))).map((doc) => doc.uuid);
|
|
81
|
+
},
|
|
82
|
+
// Without an integer id, the feed would find nothing and tombstone without proof; proven tombstones pass.
|
|
83
|
+
enqueue: (entries) => feed.enqueue(entries.filter((e) => e.tombstone || Number.isInteger(e.local?.id) || Number.isInteger(e.remote))),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { wooCouponSchema } from '../schemas/coupons';
|
|
2
|
+
import { wooProductUuid } from './products';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One coupon row as the till stores it: a coupon without a uuid cannot be keyed and is skipped, never thrown,
|
|
6
|
+
* so one bad row cannot stall the coupons' pull. A coupon that is not published arrives deleted.
|
|
7
|
+
*/
|
|
8
|
+
export function toCouponDocument(coupon: any): any | undefined {
|
|
9
|
+
const uuid = wooProductUuid(coupon);
|
|
10
|
+
if (uuid === undefined) return undefined;
|
|
11
|
+
const projected = Object.fromEntries(Object.keys(wooCouponSchema.properties)
|
|
12
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(coupon, key))
|
|
13
|
+
.map((key) => [key, coupon[key]]));
|
|
14
|
+
if (Array.isArray(projected.used_by)) projected.used_by = projected.used_by.map(String);
|
|
15
|
+
return { ...projected, uuid, _deleted: coupon.status !== 'publish' };
|
|
16
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { RxJsonSchema } from 'rxdb';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WooCommerce Coupon RxDB schema (ADR-077 amendment 2).
|
|
5
|
+
* Mirrors the coupon listing; the catalogue reconcile is its only source.
|
|
6
|
+
*/
|
|
7
|
+
export const wooCouponSchema: RxJsonSchema<any> = {
|
|
8
|
+
title: 'WooCommerce Coupon',
|
|
9
|
+
version: 0,
|
|
10
|
+
type: 'object',
|
|
11
|
+
primaryKey: 'uuid',
|
|
12
|
+
properties: {
|
|
13
|
+
uuid: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
maxLength: 36,
|
|
16
|
+
},
|
|
17
|
+
id: {
|
|
18
|
+
type: 'integer',
|
|
19
|
+
multipleOf: 1,
|
|
20
|
+
minimum: 0,
|
|
21
|
+
maximum: 2147483647,
|
|
22
|
+
},
|
|
23
|
+
code: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
maxLength: 255,
|
|
26
|
+
},
|
|
27
|
+
status: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
},
|
|
30
|
+
amount: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
},
|
|
33
|
+
discount_type: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
},
|
|
36
|
+
description: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
},
|
|
39
|
+
date_created: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
},
|
|
42
|
+
date_created_gmt: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
},
|
|
45
|
+
date_modified: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
},
|
|
48
|
+
date_modified_gmt: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
},
|
|
51
|
+
minimum_amount: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
},
|
|
54
|
+
maximum_amount: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
},
|
|
57
|
+
date_expires: {
|
|
58
|
+
type: ['string', 'null'],
|
|
59
|
+
},
|
|
60
|
+
date_expires_gmt: {
|
|
61
|
+
type: ['string', 'null'],
|
|
62
|
+
},
|
|
63
|
+
usage_count: {
|
|
64
|
+
type: 'integer',
|
|
65
|
+
},
|
|
66
|
+
usage_limit: {
|
|
67
|
+
type: ['integer', 'null'],
|
|
68
|
+
},
|
|
69
|
+
usage_limit_per_user: {
|
|
70
|
+
type: ['integer', 'null'],
|
|
71
|
+
},
|
|
72
|
+
limit_usage_to_x_items: {
|
|
73
|
+
type: ['integer', 'null'],
|
|
74
|
+
},
|
|
75
|
+
individual_use: {
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
},
|
|
78
|
+
free_shipping: {
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
},
|
|
81
|
+
exclude_sale_items: {
|
|
82
|
+
type: 'boolean',
|
|
83
|
+
},
|
|
84
|
+
product_ids: {
|
|
85
|
+
type: 'array',
|
|
86
|
+
items: { type: 'integer' },
|
|
87
|
+
},
|
|
88
|
+
excluded_product_ids: {
|
|
89
|
+
type: 'array',
|
|
90
|
+
items: { type: 'integer' },
|
|
91
|
+
},
|
|
92
|
+
product_categories: {
|
|
93
|
+
type: 'array',
|
|
94
|
+
items: { type: 'integer' },
|
|
95
|
+
},
|
|
96
|
+
excluded_product_categories: {
|
|
97
|
+
type: 'array',
|
|
98
|
+
items: { type: 'integer' },
|
|
99
|
+
},
|
|
100
|
+
email_restrictions: {
|
|
101
|
+
type: 'array',
|
|
102
|
+
items: { type: 'string' },
|
|
103
|
+
},
|
|
104
|
+
used_by: {
|
|
105
|
+
type: 'array',
|
|
106
|
+
items: { type: 'string' },
|
|
107
|
+
},
|
|
108
|
+
meta_data: {
|
|
109
|
+
type: 'array',
|
|
110
|
+
items: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
id: { type: 'integer' },
|
|
114
|
+
key: { type: 'string' },
|
|
115
|
+
value: {},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
indexes: ['code'],
|
|
121
|
+
required: ['uuid'],
|
|
122
|
+
};
|
package/src/store-settings.ts
CHANGED
|
@@ -5,6 +5,20 @@ import type { ServerCapabilities, StoreSettings, SyncContext } from '@tallyui/co
|
|
|
5
5
|
export const WOO_ORDER_CREATE_VERSION = 5;
|
|
6
6
|
// The order.create version a store gets unless /status lists order_create_v5, which only the TallyUI fork of the plugin advertises.
|
|
7
7
|
const WOO_BASE_ORDER_CREATE_VERSION = 3;
|
|
8
|
+
// The first woocommerce-pos release whose coupon recalculation keeps the POS price (ADR-077 amendment 4, R2: 1.9.0, 2026-05-15).
|
|
9
|
+
const WOO_COUPONS_MIN_PLUGIN: readonly [number, number, number] = [1, 9, 0];
|
|
10
|
+
|
|
11
|
+
export function wooPluginSupportsCoupons(version: unknown): boolean {
|
|
12
|
+
if (typeof version !== 'string') return false;
|
|
13
|
+
const match = /^(\d+)\.(\d+)(?:\.(\d+))?(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/.exec(version.trim());
|
|
14
|
+
if (!match) return false;
|
|
15
|
+
const parts: readonly [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
|
|
16
|
+
for (let index = 0; index < parts.length; index++) {
|
|
17
|
+
if (parts[index] > WOO_COUPONS_MIN_PLUGIN[index]) return true;
|
|
18
|
+
if (parts[index] < WOO_COUPONS_MIN_PLUGIN[index]) return false;
|
|
19
|
+
}
|
|
20
|
+
return !match[4];
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
export async function wooStoreSettings(context: SyncContext): Promise<StoreSettings> {
|
|
10
24
|
try {
|
|
@@ -38,6 +52,7 @@ export async function wooStoreSettings(context: SyncContext): Promise<StoreSetti
|
|
|
38
52
|
currency,
|
|
39
53
|
pricesIncludeTax: store.prices_include_tax === 'yes',
|
|
40
54
|
taxRoundAtSubtotal: store.tax_round_at_subtotal === 'yes',
|
|
55
|
+
calcDiscountsSequentially: (store.woocommerce_calc_discounts_sequentially ?? store.calc_discounts_sequentially) === 'yes',
|
|
41
56
|
...(typeof store.shipping_tax_class === 'string' ? { shippingTaxClass: store.shipping_tax_class } : {}),
|
|
42
57
|
taxClassSlugs: classes.map((row) => row.slug),
|
|
43
58
|
taxRates,
|
|
@@ -118,11 +133,24 @@ export async function readWooCapabilities(context: SyncContext): Promise<ServerC
|
|
|
118
133
|
} catch (error) {
|
|
119
134
|
if (error instanceof ConnectorUnauthorizedError) throw error;
|
|
120
135
|
}
|
|
136
|
+
let coupons = false;
|
|
137
|
+
try {
|
|
138
|
+
const site = await fetch(`${context.baseUrl}/site`, {
|
|
139
|
+
method: 'GET', headers: context.headers, signal: context.signal,
|
|
140
|
+
});
|
|
141
|
+
if (site.ok) {
|
|
142
|
+
const body = await site.json();
|
|
143
|
+
coupons = wooPluginSupportsCoupons(body?.wcpos_version);
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
coupons = false;
|
|
147
|
+
}
|
|
121
148
|
const orderCreate = capabilities.includes('order_create_v5') ? WOO_ORDER_CREATE_VERSION : WOO_BASE_ORDER_CREATE_VERSION;
|
|
122
149
|
return {
|
|
123
150
|
orderCreate,
|
|
124
151
|
taxRounding: { granularity: 'woocommerce', roundAtSubtotal: stores[0].tax_round_at_subtotal === 'yes' },
|
|
125
152
|
multiplePayments,
|
|
153
|
+
coupons,
|
|
126
154
|
// WCPOS v5 takes a fee's, shipping line's and custom line's tax_status and tax_class (orders #146, #161); it has no /tally/v1/info.
|
|
127
155
|
lineTax: { none: true, classes: true },
|
|
128
156
|
};
|