@aglyn/plugins-commerce 1.0.0-beta.244 → 1.0.0-beta.246
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 +4 -1
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +10 -0
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/product-zones.d.ts +7 -1
- package/src/lib/components/console/product-zones.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.d.ts +3 -2
- package/src/lib/components/console/products-hub-zone.component.js +9 -1
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/review-settings-card.component.d.ts +14 -0
- package/src/lib/components/console/review-settings-card.component.js +168 -0
- package/src/lib/components/console/review-settings-card.component.js.map +1 -0
- package/src/lib/components/console/reviews-moderation-card.component.d.ts +14 -0
- package/src/lib/components/console/reviews-moderation-card.component.js +85 -11
- package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -1
- package/src/lib/components/product-reviews.d.ts +29 -1
- package/src/lib/components/product-reviews.js +97 -12
- package/src/lib/components/product-reviews.js.map +1 -1
- package/src/lib/model/commerce.d.ts +11 -2
- package/src/lib/model/commerce.js +14 -2
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/product-reviews.d.ts +124 -0
- package/src/lib/model/product-reviews.js +133 -0
- package/src/lib/model/product-reviews.js.map +1 -0
- package/src/lib/model/store-setup-content.d.ts +145 -0
- package/src/lib/model/store-setup-content.js +813 -0
- package/src/lib/model/store-setup-content.js.map +1 -0
- package/src/lib/server/membership-reset.js +8 -2
- package/src/lib/server/membership-reset.js.map +1 -1
- package/src/lib/server/order-event-triggers.d.ts +2 -0
- package/src/lib/server/order-event-triggers.js +18 -1
- package/src/lib/server/order-event-triggers.js.map +1 -1
- package/src/lib/server/review-requests.d.ts +102 -0
- package/src/lib/server/review-requests.js +346 -0
- package/src/lib/server/review-requests.js.map +1 -0
- package/src/lib/server/review-tokens.d.ts +82 -0
- package/src/lib/server/review-tokens.js +119 -0
- package/src/lib/server/review-tokens.js.map +1 -0
- package/src/lib/server/reviews.d.ts +36 -3
- package/src/lib/server/reviews.js +310 -52
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/store-setup-ai-capability.d.ts +61 -0
- package/src/lib/server/store-setup-ai-capability.js +343 -0
- package/src/lib/server/store-setup-ai-capability.js.map +1 -0
- package/src/lib/server/store-setup-ai-writer.console.d.ts +27 -0
- package/src/lib/server/store-setup-ai-writer.console.js +44 -0
- package/src/lib/server/store-setup-ai-writer.console.js.map +1 -0
- package/src/lib/server/store-setup-ai-writer.d.ts +37 -0
- package/src/lib/server/store-setup-ai-writer.js +174 -0
- package/src/lib/server/store-setup-ai-writer.js.map +1 -0
- package/src/lib/server.js +27 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.js +58 -0
- package/src/lib/tenant-emails.js.map +1 -1
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { invalidOpeningHoursLines } from "@aglyn/aglyn/app-utils/local-business";
|
|
18
|
+
import { COMMERCE_MAX_PRICE_USD } from "./commerce.js";
|
|
19
|
+
import { summarizeShippingCoverage } from "./commerce-shipping.js";
|
|
20
|
+
import { storefrontTaxMisconfiguration } from "./commerce-tax-decision.js";
|
|
21
|
+
import { LOCAL_DELIVERY_POSTCODES_MAX, LOCAL_DELIVERY_ZONES_MAX, PICKUP_HOURS_MAX, PICKUP_INSTRUCTIONS_MAX, localDeliveryProblems, normalizeLocalDeliverySettings, normalizePickupSettings } from "./local-fulfillment-settings.js";
|
|
22
|
+
/*==========================================
|
|
23
|
+
* STORE SETUP AN AI BUILD CHANGES (AGL-3749)
|
|
24
|
+
*
|
|
25
|
+
* One change to the store's shipping, pickup, local delivery or tax settings,
|
|
26
|
+
* as the `store-settings` writer receives it, and the pure step that turns
|
|
27
|
+
* the settings a site has into the settings it has after the change.
|
|
28
|
+
*
|
|
29
|
+
* Every field is one the console's cards already save, in the shape they
|
|
30
|
+
* save it:
|
|
31
|
+
*
|
|
32
|
+
* - `shipping`: a zone (`settings/store.shipping.zones`) and one rate in it
|
|
33
|
+
* (`.rates`), priced flat, free over a subtotal, or by subtotal or weight
|
|
34
|
+
* tiers — the Shipping card's four kinds. Carrier rates are not offered:
|
|
35
|
+
* they quote through a carrier account a person connects.
|
|
36
|
+
* - `pickup`: the store-wide "Offer free local pickup" switch
|
|
37
|
+
* (`shipping.localPickup`), or pickup at one of the store's inventory
|
|
38
|
+
* locations (`locations/{id}.pickup`), as the Inventory locations card
|
|
39
|
+
* saves it. A location is found, never created.
|
|
40
|
+
* - `local-delivery`: the Local delivery card's settings
|
|
41
|
+
* (`settings/store.localDelivery`) with postal-code zones. Distance zones
|
|
42
|
+
* need the store placed on a map first, which is the card's to do.
|
|
43
|
+
* - `tax`: how the store handles sales tax (`settings/store.tax.mode`),
|
|
44
|
+
* whether prices include it, the store origin and manual rates. The
|
|
45
|
+
* lodging and service rates are kept as they are.
|
|
46
|
+
*
|
|
47
|
+
* Nothing here names a price or a rate the change did not carry: a blank
|
|
48
|
+
* flat rate charges nothing at checkout, so a flat or free-over rate needs
|
|
49
|
+
* its amount, and a tax rate needs its percentage. Pure.
|
|
50
|
+
*=========================================*/ /** The resource the writer is registered under; the commerce record route `store-settings` links it. */ export const STORE_SETUP_RESOURCE = 'store-settings';
|
|
51
|
+
export const STORE_SETUP_SECTIONS = [
|
|
52
|
+
'shipping',
|
|
53
|
+
'pickup',
|
|
54
|
+
'local-delivery',
|
|
55
|
+
'tax'
|
|
56
|
+
];
|
|
57
|
+
/** The rate kinds an AI change may set: every kind of the Shipping card except carrier rates. */ export const STORE_SHIPPING_AI_KINDS = [
|
|
58
|
+
'flat',
|
|
59
|
+
'free_over',
|
|
60
|
+
'price_tiers',
|
|
61
|
+
'weight_tiers'
|
|
62
|
+
];
|
|
63
|
+
export const STORE_SHIPPING_ACTIONS = [
|
|
64
|
+
'set',
|
|
65
|
+
'remove'
|
|
66
|
+
];
|
|
67
|
+
export const STORE_TAX_MODES = [
|
|
68
|
+
'manual',
|
|
69
|
+
'stripe',
|
|
70
|
+
'none'
|
|
71
|
+
];
|
|
72
|
+
export const STORE_SETUP_NAME_MAX = 80;
|
|
73
|
+
export const STORE_SETUP_COUNTRIES_MAX = 60;
|
|
74
|
+
export const STORE_SHIPPING_TIERS_MAX = 10;
|
|
75
|
+
export const STORE_SETUP_LOCATION_MAX = 80;
|
|
76
|
+
export const STORE_TAX_LABEL_MAX = 60;
|
|
77
|
+
/** Grams; the heaviest tier a weight table may name (1,000 kg). */ export const STORE_SHIPPING_MAX_GRAMS = 1000000;
|
|
78
|
+
/** Two weeks, the longest preparation or lead time the cards keep. */ export const STORE_SETUP_MAX_MINUTES = 14 * 24 * 60;
|
|
79
|
+
const COUNTRY = /^[A-Z]{2}$/;
|
|
80
|
+
const STATE = /^[A-Z0-9]{1,3}$/;
|
|
81
|
+
const has = (raw, key)=>raw[key] !== undefined && raw[key] !== null && raw[key] !== '';
|
|
82
|
+
function text(raw, key, max, problems) {
|
|
83
|
+
if (!has(raw, key)) return undefined;
|
|
84
|
+
const value = raw[key];
|
|
85
|
+
if (typeof value !== 'string') {
|
|
86
|
+
problems.push(`"${key}" must be text`);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const trimmed = value.trim();
|
|
90
|
+
if (trimmed.length > max) problems.push(`"${key}" must be at most ${max} characters`);
|
|
91
|
+
return trimmed || undefined;
|
|
92
|
+
}
|
|
93
|
+
function bool(raw, key, problems) {
|
|
94
|
+
if (!has(raw, key)) return undefined;
|
|
95
|
+
if (typeof raw[key] !== 'boolean') {
|
|
96
|
+
problems.push(`"${key}" must be true or false`);
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return raw[key];
|
|
100
|
+
}
|
|
101
|
+
function number(raw, key, min, max, problems) {
|
|
102
|
+
if (!has(raw, key)) return undefined;
|
|
103
|
+
const value = raw[key];
|
|
104
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
105
|
+
problems.push(`"${key}" must be a number`);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
if (value < min || value > max) {
|
|
109
|
+
problems.push(`"${key}" must be between ${min} and ${max}`);
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
function minutes(raw, key, problems) {
|
|
115
|
+
const value = number(raw, key, 0, STORE_SETUP_MAX_MINUTES, problems);
|
|
116
|
+
return value === undefined ? undefined : Math.round(value);
|
|
117
|
+
}
|
|
118
|
+
/** Dollars to the cents the settings store. */ function cents(raw, key, problems) {
|
|
119
|
+
const value = number(raw, key, 0, COMMERCE_MAX_PRICE_USD, problems);
|
|
120
|
+
return value === undefined ? undefined : Math.round(value * 100);
|
|
121
|
+
}
|
|
122
|
+
function list(raw, key, max, problems) {
|
|
123
|
+
if (!has(raw, key)) return undefined;
|
|
124
|
+
const value = raw[key];
|
|
125
|
+
if (!Array.isArray(value) || value.some((one)=>typeof one !== 'string')) {
|
|
126
|
+
problems.push(`"${key}" must be a list of text`);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const items = value.map((one)=>one.trim()).filter(Boolean);
|
|
130
|
+
if (items.length > max) problems.push(`"${key}" must hold at most ${max}`);
|
|
131
|
+
return items;
|
|
132
|
+
}
|
|
133
|
+
function countryCode(raw, key, problems) {
|
|
134
|
+
var _text;
|
|
135
|
+
const value = (_text = text(raw, key, 2, problems)) == null ? void 0 : _text.toUpperCase();
|
|
136
|
+
if (value !== undefined && !COUNTRY.test(value)) problems.push(`"${key}" must be a two-letter country code like US`);
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
function stateCode(raw, key, problems) {
|
|
140
|
+
var _text;
|
|
141
|
+
const value = (_text = text(raw, key, 3, problems)) == null ? void 0 : _text.toUpperCase();
|
|
142
|
+
if (value !== undefined && !STATE.test(value)) problems.push(`"${key}" must be a state or region code like TX`);
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
/** Opening-hours lines; a `;` separates lines as a newline does. */ function hoursText(raw, key, problems) {
|
|
146
|
+
const value = text(raw, key, PICKUP_HOURS_MAX, problems);
|
|
147
|
+
if (value === undefined) return undefined;
|
|
148
|
+
const lines = value.split(/\s*(?:;|\r?\n)\s*/).filter(Boolean).join('\n');
|
|
149
|
+
const bad = invalidOpeningHoursLines(lines);
|
|
150
|
+
if (bad.length) {
|
|
151
|
+
problems.push(`"${key}" must be lines like "Mo-Fr 09:00-17:00" or "Sa 10:00-14:00"`);
|
|
152
|
+
}
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
/** "50:5" → up to 50 (dollars, or grams for weight) costs $5. */ function tiersOf(raw, kind, problems) {
|
|
156
|
+
const entries = list(raw, 'tiers', STORE_SHIPPING_TIERS_MAX, problems);
|
|
157
|
+
if (entries === undefined) return undefined;
|
|
158
|
+
const weight = kind === 'weight_tiers';
|
|
159
|
+
const tiers = [];
|
|
160
|
+
for (const entry of entries){
|
|
161
|
+
const match = /^\s*\$?([0-9]+(?:\.[0-9]+)?)\s*(?:g)?\s*:\s*\$?([0-9]+(?:\.[0-9]+)?)\s*$/.exec(entry);
|
|
162
|
+
const upTo = match ? Number(match[1]) : NaN;
|
|
163
|
+
const amount = match ? Number(match[2]) : NaN;
|
|
164
|
+
if (!match || !(upTo > 0) || amount > COMMERCE_MAX_PRICE_USD || weight && upTo > STORE_SHIPPING_MAX_GRAMS) {
|
|
165
|
+
problems.push(`"tiers" holds "${entry}", which is not "up to:price" like "50:5"`);
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
tiers.push({
|
|
169
|
+
upTo: weight ? Math.round(upTo) : Math.round(upTo * 100),
|
|
170
|
+
amountCents: Math.round(amount * 100)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
tiers.sort((a, b)=>a.upTo - b.upTo);
|
|
174
|
+
if (tiers.some((tier, index)=>index > 0 && tier.upTo === tiers[index - 1].upTo)) {
|
|
175
|
+
problems.push('"tiers" names the same limit twice');
|
|
176
|
+
}
|
|
177
|
+
return tiers;
|
|
178
|
+
}
|
|
179
|
+
const KNOWN = {
|
|
180
|
+
shipping: [
|
|
181
|
+
'action',
|
|
182
|
+
'zoneName',
|
|
183
|
+
'countries',
|
|
184
|
+
'rateName',
|
|
185
|
+
'kind',
|
|
186
|
+
'amountUsd',
|
|
187
|
+
'freeOverUsd',
|
|
188
|
+
'tiers'
|
|
189
|
+
],
|
|
190
|
+
pickup: [
|
|
191
|
+
'location',
|
|
192
|
+
'enabled',
|
|
193
|
+
'hours',
|
|
194
|
+
'instructions',
|
|
195
|
+
'readyWithinMinutes'
|
|
196
|
+
],
|
|
197
|
+
'local-delivery': [
|
|
198
|
+
'enabled',
|
|
199
|
+
'country',
|
|
200
|
+
'location',
|
|
201
|
+
'zoneName',
|
|
202
|
+
'postcodes',
|
|
203
|
+
'feeUsd',
|
|
204
|
+
'minimumUsd',
|
|
205
|
+
'freeOverUsd',
|
|
206
|
+
'removeZone',
|
|
207
|
+
'windows',
|
|
208
|
+
'leadTimeMinutes',
|
|
209
|
+
'daysAhead',
|
|
210
|
+
'instructions'
|
|
211
|
+
],
|
|
212
|
+
tax: [
|
|
213
|
+
'mode',
|
|
214
|
+
'pricesIncludeTax',
|
|
215
|
+
'originCountry',
|
|
216
|
+
'originState',
|
|
217
|
+
'rateCountry',
|
|
218
|
+
'rateState',
|
|
219
|
+
'ratePercent',
|
|
220
|
+
'rateLabel',
|
|
221
|
+
'removeRate'
|
|
222
|
+
]
|
|
223
|
+
};
|
|
224
|
+
function readShipping(raw, problems) {
|
|
225
|
+
var _list;
|
|
226
|
+
const action = has(raw, 'action') ? raw['action'] : 'set';
|
|
227
|
+
if (!STORE_SHIPPING_ACTIONS.includes(action)) problems.push('"action" must be set or remove');
|
|
228
|
+
const zoneName = text(raw, 'zoneName', STORE_SETUP_NAME_MAX, problems);
|
|
229
|
+
if (!zoneName) problems.push('"zoneName" is required: the shipping zone, like "United States"');
|
|
230
|
+
const countries = (_list = list(raw, 'countries', STORE_SETUP_COUNTRIES_MAX, problems)) == null ? void 0 : _list.map((code)=>code.toUpperCase());
|
|
231
|
+
for (const code of countries != null ? countries : []){
|
|
232
|
+
if (code !== '*' && !COUNTRY.test(code)) {
|
|
233
|
+
problems.push(`"countries" holds "${code}", which is not a two-letter country code or *`);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const rateName = text(raw, 'rateName', STORE_SETUP_NAME_MAX, problems);
|
|
238
|
+
const kind = has(raw, 'kind') ? raw['kind'] : undefined;
|
|
239
|
+
if (kind !== undefined && !STORE_SHIPPING_AI_KINDS.includes(kind)) {
|
|
240
|
+
problems.push(`"kind" must be one of ${STORE_SHIPPING_AI_KINDS.join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
const amountCents = cents(raw, 'amountUsd', problems);
|
|
243
|
+
const freeOverCents = cents(raw, 'freeOverUsd', problems);
|
|
244
|
+
const tiers = tiersOf(raw, kind, problems);
|
|
245
|
+
if (action === 'set') {
|
|
246
|
+
// A rate with no name is dropped at checkout (AGL-1791), and a blank
|
|
247
|
+
// amount charges nothing: both are the person's to state.
|
|
248
|
+
if (!rateName) problems.push('"rateName" is required: what the shopper sees, like "Standard"');
|
|
249
|
+
if (!kind) problems.push('"kind" is required');
|
|
250
|
+
if ((kind === 'flat' || kind === 'free_over') && amountCents === undefined) {
|
|
251
|
+
problems.push(`"amountUsd" is required for a ${kind === 'flat' ? 'flat' : 'free-over'} rate`);
|
|
252
|
+
}
|
|
253
|
+
if (kind === 'free_over' && !freeOverCents) problems.push('"freeOverUsd" is required for a free-over rate');
|
|
254
|
+
if ((kind === 'price_tiers' || kind === 'weight_tiers') && !(tiers == null ? void 0 : tiers.length)) {
|
|
255
|
+
problems.push('"tiers" is required for a tiered rate');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return _extends({
|
|
259
|
+
section: 'shipping',
|
|
260
|
+
action: action,
|
|
261
|
+
zoneName: zoneName != null ? zoneName : ''
|
|
262
|
+
}, countries ? {
|
|
263
|
+
countries
|
|
264
|
+
} : {}, rateName ? {
|
|
265
|
+
rateName
|
|
266
|
+
} : {}, kind ? {
|
|
267
|
+
kind
|
|
268
|
+
} : {}, amountCents !== undefined ? {
|
|
269
|
+
amountCents
|
|
270
|
+
} : {}, freeOverCents !== undefined ? {
|
|
271
|
+
freeOverCents
|
|
272
|
+
} : {}, tiers ? {
|
|
273
|
+
tiers
|
|
274
|
+
} : {});
|
|
275
|
+
}
|
|
276
|
+
function readPickup(raw, problems) {
|
|
277
|
+
var _bool;
|
|
278
|
+
const location = text(raw, 'location', STORE_SETUP_LOCATION_MAX, problems);
|
|
279
|
+
const hours = hoursText(raw, 'hours', problems);
|
|
280
|
+
const instructions = text(raw, 'instructions', PICKUP_INSTRUCTIONS_MAX, problems);
|
|
281
|
+
const readyWithinMinutes = minutes(raw, 'readyWithinMinutes', problems);
|
|
282
|
+
if (!location && (hours !== undefined || instructions !== undefined || readyWithinMinutes !== undefined)) {
|
|
283
|
+
problems.push('Pickup hours, instructions and preparation time belong to a location: name the inventory location');
|
|
284
|
+
}
|
|
285
|
+
return _extends({
|
|
286
|
+
section: 'pickup',
|
|
287
|
+
enabled: (_bool = bool(raw, 'enabled', problems)) != null ? _bool : true
|
|
288
|
+
}, location ? {
|
|
289
|
+
location
|
|
290
|
+
} : {}, hours !== undefined ? {
|
|
291
|
+
hours
|
|
292
|
+
} : {}, instructions !== undefined ? {
|
|
293
|
+
instructions
|
|
294
|
+
} : {}, readyWithinMinutes !== undefined ? {
|
|
295
|
+
readyWithinMinutes
|
|
296
|
+
} : {});
|
|
297
|
+
}
|
|
298
|
+
function readLocalDelivery(raw, problems) {
|
|
299
|
+
var _bool;
|
|
300
|
+
const zoneName = text(raw, 'zoneName', STORE_SETUP_NAME_MAX, problems);
|
|
301
|
+
const postcodes = list(raw, 'postcodes', LOCAL_DELIVERY_POSTCODES_MAX, problems);
|
|
302
|
+
const feeCents = cents(raw, 'feeUsd', problems);
|
|
303
|
+
const minimumCents = cents(raw, 'minimumUsd', problems);
|
|
304
|
+
const freeOverCents = cents(raw, 'freeOverUsd', problems);
|
|
305
|
+
const removeZone = bool(raw, 'removeZone', problems);
|
|
306
|
+
if (!zoneName && (postcodes || feeCents !== undefined || minimumCents !== undefined || freeOverCents !== undefined || removeZone)) {
|
|
307
|
+
problems.push('"zoneName" is required to add, change or remove a delivery zone');
|
|
308
|
+
}
|
|
309
|
+
const daysAhead = number(raw, 'daysAhead', 1, 30, problems);
|
|
310
|
+
return _extends({
|
|
311
|
+
section: 'local-delivery',
|
|
312
|
+
enabled: (_bool = bool(raw, 'enabled', problems)) != null ? _bool : true
|
|
313
|
+
}, has(raw, 'country') ? {
|
|
314
|
+
country: countryCode(raw, 'country', problems)
|
|
315
|
+
} : {}, has(raw, 'location') ? {
|
|
316
|
+
location: text(raw, 'location', STORE_SETUP_LOCATION_MAX, problems)
|
|
317
|
+
} : {}, zoneName ? {
|
|
318
|
+
zoneName
|
|
319
|
+
} : {}, postcodes ? {
|
|
320
|
+
postcodes
|
|
321
|
+
} : {}, feeCents !== undefined ? {
|
|
322
|
+
feeCents
|
|
323
|
+
} : {}, minimumCents !== undefined ? {
|
|
324
|
+
minimumCents
|
|
325
|
+
} : {}, freeOverCents !== undefined ? {
|
|
326
|
+
freeOverCents
|
|
327
|
+
} : {}, removeZone ? {
|
|
328
|
+
removeZone
|
|
329
|
+
} : {}, has(raw, 'windows') ? {
|
|
330
|
+
windows: hoursText(raw, 'windows', problems)
|
|
331
|
+
} : {}, has(raw, 'leadTimeMinutes') ? {
|
|
332
|
+
leadTimeMinutes: minutes(raw, 'leadTimeMinutes', problems)
|
|
333
|
+
} : {}, daysAhead !== undefined ? {
|
|
334
|
+
daysAhead: Math.round(daysAhead)
|
|
335
|
+
} : {}, has(raw, 'instructions') ? {
|
|
336
|
+
instructions: text(raw, 'instructions', PICKUP_INSTRUCTIONS_MAX, problems)
|
|
337
|
+
} : {});
|
|
338
|
+
}
|
|
339
|
+
function readTax(raw, problems) {
|
|
340
|
+
const mode = has(raw, 'mode') ? raw['mode'] : undefined;
|
|
341
|
+
if (mode !== undefined && !STORE_TAX_MODES.includes(mode)) {
|
|
342
|
+
problems.push(`"mode" must be one of ${STORE_TAX_MODES.join(', ')}`);
|
|
343
|
+
}
|
|
344
|
+
const change = _extends({
|
|
345
|
+
section: 'tax'
|
|
346
|
+
}, mode !== undefined ? {
|
|
347
|
+
mode: mode
|
|
348
|
+
} : {});
|
|
349
|
+
const pricesIncludeTax = bool(raw, 'pricesIncludeTax', problems);
|
|
350
|
+
if (pricesIncludeTax !== undefined) change.pricesIncludeTax = pricesIncludeTax;
|
|
351
|
+
const originCountry = countryCode(raw, 'originCountry', problems);
|
|
352
|
+
if (originCountry) change.originCountry = originCountry;
|
|
353
|
+
const originState = stateCode(raw, 'originState', problems);
|
|
354
|
+
if (originState) change.originState = originState;
|
|
355
|
+
const rateCountry = countryCode(raw, 'rateCountry', problems);
|
|
356
|
+
if (rateCountry) change.rateCountry = rateCountry;
|
|
357
|
+
const rateState = stateCode(raw, 'rateState', problems);
|
|
358
|
+
if (rateState) change.rateState = rateState;
|
|
359
|
+
if (has(raw, 'ratePercent')) {
|
|
360
|
+
const value = raw['ratePercent'];
|
|
361
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 100) {
|
|
362
|
+
problems.push('"ratePercent" must be a percentage above 0 and at most 100');
|
|
363
|
+
} else change.ratePct = value;
|
|
364
|
+
}
|
|
365
|
+
const rateLabel = text(raw, 'rateLabel', STORE_TAX_LABEL_MAX, problems);
|
|
366
|
+
if (rateLabel) change.rateLabel = rateLabel;
|
|
367
|
+
const removeRate = bool(raw, 'removeRate', problems);
|
|
368
|
+
if (removeRate) change.removeRate = true;
|
|
369
|
+
const touchesRate = change.ratePct !== undefined || change.removeRate || change.rateState || change.rateLabel;
|
|
370
|
+
if (touchesRate && !change.rateCountry) problems.push('"rateCountry" is required with a tax rate');
|
|
371
|
+
if (change.rateCountry && change.ratePct === undefined && !change.removeRate) {
|
|
372
|
+
// Never a rate the person did not state.
|
|
373
|
+
problems.push('"ratePercent" is required with a tax rate: the percentage the request states');
|
|
374
|
+
}
|
|
375
|
+
if (change.ratePct !== undefined && change.removeRate) problems.push('Give "ratePercent" or "removeRate", not both');
|
|
376
|
+
if (originState && !originCountry) problems.push('"originCountry" is required with "originState"');
|
|
377
|
+
const said = Object.keys(change).filter((key)=>key !== 'section');
|
|
378
|
+
if (!said.length) {
|
|
379
|
+
problems.push('Say how the store handles sales tax: manual rates (with the percentage), automatic calculation, or not collecting it');
|
|
380
|
+
}
|
|
381
|
+
return change;
|
|
382
|
+
}
|
|
383
|
+
/** Reads a change from content; the problems name what is wrong, in words a person reads. */ export function readStoreSetupChange(content) {
|
|
384
|
+
const section = content['section'];
|
|
385
|
+
if (!STORE_SETUP_SECTIONS.includes(section)) {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
problems: [
|
|
389
|
+
`"section" must be one of ${STORE_SETUP_SECTIONS.join(', ')}`
|
|
390
|
+
]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const problems = [];
|
|
394
|
+
for (const key of Object.keys(content)){
|
|
395
|
+
if (key !== 'section' && !KNOWN[section].includes(key)) problems.push(`"${key}" is not a ${section} setting`);
|
|
396
|
+
}
|
|
397
|
+
const value = section === 'shipping' ? readShipping(content, problems) : section === 'pickup' ? readPickup(content, problems) : section === 'local-delivery' ? readLocalDelivery(content, problems) : readTax(content, problems);
|
|
398
|
+
return problems.length ? {
|
|
399
|
+
ok: false,
|
|
400
|
+
problems
|
|
401
|
+
} : {
|
|
402
|
+
ok: true,
|
|
403
|
+
value
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const fail = (status, error)=>({
|
|
407
|
+
ok: false,
|
|
408
|
+
status,
|
|
409
|
+
error
|
|
410
|
+
});
|
|
411
|
+
/** Key-order-blind equality over plain JSON values. */ export function sameSetting(a, b) {
|
|
412
|
+
if (a === b) return true;
|
|
413
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
414
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((one, index)=>sameSetting(one, b[index]));
|
|
415
|
+
}
|
|
416
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
417
|
+
const left = Object.entries(a).filter(([, value])=>value !== undefined);
|
|
418
|
+
const right = Object.entries(b).filter(([, value])=>value !== undefined);
|
|
419
|
+
return left.length === right.length && left.every(([key, value])=>sameSetting(value, b[key]));
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
const lower = (value)=>String(value != null ? value : '').trim().toLowerCase();
|
|
424
|
+
const usd = (centsValue)=>`$${(centsValue / 100).toFixed(centsValue % 100 ? 2 : 0)}`;
|
|
425
|
+
const sameSet = (a, b)=>a.length === b.length && [
|
|
426
|
+
...a
|
|
427
|
+
].sort().join(',') === [
|
|
428
|
+
...b
|
|
429
|
+
].sort().join(',');
|
|
430
|
+
/** The store's locations a phrase names: by exact name first, then by city or address. */ export function matchStoreLocations(locations, phrase) {
|
|
431
|
+
const wanted = lower(phrase);
|
|
432
|
+
const byName = locations.filter((one)=>lower(one.name) === wanted);
|
|
433
|
+
if (byName.length) return byName;
|
|
434
|
+
return locations.filter((one)=>lower(one.city) === wanted || wanted.length >= 3 && lower(one.address).includes(wanted) || lower(one.name).includes(wanted));
|
|
435
|
+
}
|
|
436
|
+
function findLocation(locations, phrase) {
|
|
437
|
+
const found = matchStoreLocations(locations, phrase);
|
|
438
|
+
if (!found.length) {
|
|
439
|
+
return fail(404, `The store has no inventory location matching "${phrase}". Add it under Products → Inventory locations, then ask again.`);
|
|
440
|
+
}
|
|
441
|
+
if (found.length > 1) {
|
|
442
|
+
return fail(409, `More than one location matches "${phrase}" (${found.map((one)=>one.name || one.id).join(', ')}); say which`);
|
|
443
|
+
}
|
|
444
|
+
return found[0];
|
|
445
|
+
}
|
|
446
|
+
function rateSummary(rate) {
|
|
447
|
+
switch(rate.kind){
|
|
448
|
+
case 'flat':
|
|
449
|
+
return rate.amountCents ? `${usd(rate.amountCents)} flat` : 'free';
|
|
450
|
+
case 'free_over':
|
|
451
|
+
var _rate_freeOverCents, _rate_amountCents;
|
|
452
|
+
return `free over ${usd((_rate_freeOverCents = rate.freeOverCents) != null ? _rate_freeOverCents : 0)}, otherwise ${usd((_rate_amountCents = rate.amountCents) != null ? _rate_amountCents : 0)}`;
|
|
453
|
+
case 'price_tiers':
|
|
454
|
+
var _rate_tiers;
|
|
455
|
+
return `${((_rate_tiers = rate.tiers) != null ? _rate_tiers : []).length} subtotal tiers`;
|
|
456
|
+
case 'weight_tiers':
|
|
457
|
+
var _rate_tiers1;
|
|
458
|
+
return `${((_rate_tiers1 = rate.tiers) != null ? _rate_tiers1 : []).length} weight tiers`;
|
|
459
|
+
default:
|
|
460
|
+
return rate.kind;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function applyShipping(state, change, ids) {
|
|
464
|
+
var _state_settings_shipping, _current_zones, _current_rates, _ref, _ref1;
|
|
465
|
+
const current = (_state_settings_shipping = state.settings['shipping']) != null ? _state_settings_shipping : {};
|
|
466
|
+
const zones = [
|
|
467
|
+
...(_current_zones = current.zones) != null ? _current_zones : []
|
|
468
|
+
];
|
|
469
|
+
const rates = [
|
|
470
|
+
...(_current_rates = current.rates) != null ? _current_rates : []
|
|
471
|
+
];
|
|
472
|
+
const zoneIndex = (()=>{
|
|
473
|
+
var _change_countries;
|
|
474
|
+
const byName = zones.findIndex((zone)=>lower(zone.name) === lower(change.zoneName));
|
|
475
|
+
if (byName >= 0 || !((_change_countries = change.countries) == null ? void 0 : _change_countries.length)) return byName;
|
|
476
|
+
return zones.findIndex((zone)=>{
|
|
477
|
+
var _zone_countries, _change_countries;
|
|
478
|
+
return sameSet(((_zone_countries = zone.countries) != null ? _zone_countries : []).map((code)=>String(code).trim().toUpperCase()), (_change_countries = change.countries) != null ? _change_countries : []);
|
|
479
|
+
});
|
|
480
|
+
})();
|
|
481
|
+
const zone = zoneIndex >= 0 ? zones[zoneIndex] : undefined;
|
|
482
|
+
if (change.action === 'remove') {
|
|
483
|
+
if (!zone) return {
|
|
484
|
+
ok: true,
|
|
485
|
+
write: null,
|
|
486
|
+
summary: `There is no "${change.zoneName}" shipping zone.`,
|
|
487
|
+
warnings: []
|
|
488
|
+
};
|
|
489
|
+
if (change.rateName) {
|
|
490
|
+
const kept = rates.filter((rate)=>!(rate.zoneId === zone.id && lower(rate.name) === lower(change.rateName)));
|
|
491
|
+
if (kept.length === rates.length) {
|
|
492
|
+
return {
|
|
493
|
+
ok: true,
|
|
494
|
+
write: null,
|
|
495
|
+
summary: `"${zone.name}" has no "${change.rateName}" rate.`,
|
|
496
|
+
warnings: []
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return shippingResult(current, _extends({}, current, {
|
|
500
|
+
zones,
|
|
501
|
+
rates: kept
|
|
502
|
+
}), `Removed the "${change.rateName}" rate from ${zone.name}.`);
|
|
503
|
+
}
|
|
504
|
+
// The card's ✕ on a zone: the zone and its rates go together.
|
|
505
|
+
return shippingResult(current, _extends({}, current, {
|
|
506
|
+
zones: zones.filter((one)=>one.id !== zone.id),
|
|
507
|
+
rates: rates.filter((rate)=>rate.zoneId !== zone.id)
|
|
508
|
+
}), `Removed the ${zone.name} shipping zone and its rates.`);
|
|
509
|
+
}
|
|
510
|
+
let zoneId;
|
|
511
|
+
if (zone) {
|
|
512
|
+
var _zone_countries;
|
|
513
|
+
var _change_countries;
|
|
514
|
+
zoneId = zone.id;
|
|
515
|
+
if (((_change_countries = change.countries) == null ? void 0 : _change_countries.length) && !sameSet((_zone_countries = zone.countries) != null ? _zone_countries : [], change.countries)) {
|
|
516
|
+
zones[zoneIndex] = _extends({}, zone, {
|
|
517
|
+
countries: [
|
|
518
|
+
...change.countries
|
|
519
|
+
]
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
var _change_countries1;
|
|
524
|
+
if (!((_change_countries1 = change.countries) == null ? void 0 : _change_countries1.length)) {
|
|
525
|
+
return fail(400, `Say which countries the new "${change.zoneName}" zone ships to (two-letter codes, or * for everywhere else)`);
|
|
526
|
+
}
|
|
527
|
+
zoneId = ids.zoneId;
|
|
528
|
+
zones.push({
|
|
529
|
+
id: zoneId,
|
|
530
|
+
name: change.zoneName,
|
|
531
|
+
countries: [
|
|
532
|
+
...change.countries
|
|
533
|
+
]
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
const rateIndex = rates.findIndex((rate)=>rate.zoneId === zoneId && lower(rate.name) === lower(change.rateName));
|
|
537
|
+
const existing = rateIndex >= 0 ? rates[rateIndex] : undefined;
|
|
538
|
+
if ((existing == null ? void 0 : existing.kind) === 'carrier') {
|
|
539
|
+
return fail(409, `"${existing.name}" is a carrier rate; change it in Shipping settings`);
|
|
540
|
+
}
|
|
541
|
+
const kind = change.kind;
|
|
542
|
+
const rate = _extends({
|
|
543
|
+
id: (_ref = existing == null ? void 0 : existing.id) != null ? _ref : ids.rateId,
|
|
544
|
+
zoneId,
|
|
545
|
+
name: change.rateName,
|
|
546
|
+
kind
|
|
547
|
+
}, kind === 'flat' || kind === 'free_over' ? {
|
|
548
|
+
amountCents: change.amountCents
|
|
549
|
+
} : {}, kind === 'free_over' ? {
|
|
550
|
+
freeOverCents: change.freeOverCents
|
|
551
|
+
} : {}, kind === 'price_tiers' || kind === 'weight_tiers' ? {
|
|
552
|
+
tiers: change.tiers
|
|
553
|
+
} : {});
|
|
554
|
+
if (rateIndex >= 0) rates[rateIndex] = rate;
|
|
555
|
+
else rates.push(rate);
|
|
556
|
+
const zoneName = (_ref1 = zone == null ? void 0 : zone.name) != null ? _ref1 : change.zoneName;
|
|
557
|
+
return shippingResult(current, _extends({}, current, {
|
|
558
|
+
zones,
|
|
559
|
+
rates
|
|
560
|
+
}), `${zoneName}: "${rate.name}" is ${rateSummary(rate)}.`, rate);
|
|
561
|
+
}
|
|
562
|
+
function shippingResult(before, after, summary, changed) {
|
|
563
|
+
const coverage = summarizeShippingCoverage(after);
|
|
564
|
+
const warnings = [];
|
|
565
|
+
if (coverage.unserved.length && !coverage.shipsNowhere) {
|
|
566
|
+
warnings.push(`Checkout turns away buyers in ${coverage.unserved.join(', ')}: no rate reaches them.`);
|
|
567
|
+
}
|
|
568
|
+
if ((changed == null ? void 0 : changed.kind) === 'price_tiers' || (changed == null ? void 0 : changed.kind) === 'weight_tiers') {
|
|
569
|
+
warnings.push(`"${changed.name}" applies only up to its last tier; a larger or heavier order gets no rate from it.`);
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
ok: true,
|
|
573
|
+
write: sameSetting(before, after) ? null : {
|
|
574
|
+
target: 'settings',
|
|
575
|
+
field: 'shipping',
|
|
576
|
+
value: after
|
|
577
|
+
},
|
|
578
|
+
summary,
|
|
579
|
+
warnings
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function applyPickup(state, change) {
|
|
583
|
+
if (!change.location) {
|
|
584
|
+
var _state_settings_shipping;
|
|
585
|
+
const current = (_state_settings_shipping = state.settings['shipping']) != null ? _state_settings_shipping : {};
|
|
586
|
+
const after = _extends({}, current, {
|
|
587
|
+
localPickup: change.enabled
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
ok: true,
|
|
591
|
+
write: Boolean(current.localPickup) === change.enabled ? null : {
|
|
592
|
+
target: 'settings',
|
|
593
|
+
field: 'shipping',
|
|
594
|
+
value: after
|
|
595
|
+
},
|
|
596
|
+
summary: change.enabled ? 'Checkout offers free local pickup.' : 'Checkout no longer offers free local pickup.',
|
|
597
|
+
warnings: change.enabled ? [
|
|
598
|
+
'Pickup is a collection choice; it does not widen where the store ships.'
|
|
599
|
+
] : []
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const found = findLocation(state.locations, change.location);
|
|
603
|
+
if ('ok' in found) return found;
|
|
604
|
+
const before = normalizePickupSettings(found.pickup);
|
|
605
|
+
const after = normalizePickupSettings(_extends({}, before, {
|
|
606
|
+
enabled: change.enabled
|
|
607
|
+
}, change.hours !== undefined ? {
|
|
608
|
+
hours: change.hours
|
|
609
|
+
} : {}, change.instructions !== undefined ? {
|
|
610
|
+
instructions: change.instructions
|
|
611
|
+
} : {}, change.readyWithinMinutes !== undefined ? {
|
|
612
|
+
readyWithinMinutes: change.readyWithinMinutes
|
|
613
|
+
} : {}));
|
|
614
|
+
const name = found.name || 'the location';
|
|
615
|
+
return {
|
|
616
|
+
ok: true,
|
|
617
|
+
write: sameSetting(before, after) ? null : {
|
|
618
|
+
target: 'location',
|
|
619
|
+
locationId: found.id,
|
|
620
|
+
value: after
|
|
621
|
+
},
|
|
622
|
+
summary: after.enabled ? `Buyers can pick up orders at ${name}.` : `Pickup at ${name} is off.`,
|
|
623
|
+
warnings: after.enabled && !after.hours ? [
|
|
624
|
+
`${name} has no pickup hours yet; buyers are not told when to come.`
|
|
625
|
+
] : []
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function applyLocalDelivery(state, change, ids) {
|
|
629
|
+
var _before_zones, _normalized_zones, _normalized_zones1;
|
|
630
|
+
var _after_zones;
|
|
631
|
+
const before = normalizeLocalDeliverySettings(state.settings['localDelivery']);
|
|
632
|
+
const after = _extends({}, before, {
|
|
633
|
+
zones: [
|
|
634
|
+
...(_before_zones = before.zones) != null ? _before_zones : []
|
|
635
|
+
]
|
|
636
|
+
});
|
|
637
|
+
if (!((_after_zones = after.zones) == null ? void 0 : _after_zones.length)) delete after.zones;
|
|
638
|
+
if (change.enabled) after.enabled = true;
|
|
639
|
+
else delete after.enabled;
|
|
640
|
+
if (change.country) after.country = change.country;
|
|
641
|
+
if (change.location) {
|
|
642
|
+
const found = findLocation(state.locations, change.location);
|
|
643
|
+
if ('ok' in found) return found;
|
|
644
|
+
after.locationId = found.id;
|
|
645
|
+
}
|
|
646
|
+
if (change.windows !== undefined) after.windows = change.windows;
|
|
647
|
+
if (change.leadTimeMinutes !== undefined) after.leadTimeMinutes = change.leadTimeMinutes;
|
|
648
|
+
if (change.daysAhead !== undefined) after.daysAhead = change.daysAhead;
|
|
649
|
+
if (change.instructions !== undefined) after.instructions = change.instructions;
|
|
650
|
+
if (change.zoneName) {
|
|
651
|
+
var _after_zones1;
|
|
652
|
+
const zones = [
|
|
653
|
+
...(_after_zones1 = after.zones) != null ? _after_zones1 : []
|
|
654
|
+
];
|
|
655
|
+
const index = zones.findIndex((zone)=>lower(zone.name) === lower(change.zoneName));
|
|
656
|
+
if (change.removeZone) {
|
|
657
|
+
if (index >= 0) zones.splice(index, 1);
|
|
658
|
+
} else if (index >= 0) {
|
|
659
|
+
const zone = zones[index];
|
|
660
|
+
if (change.postcodes && zone.kind === 'radius') {
|
|
661
|
+
return fail(409, `"${zone.name}" is a distance zone; change it under Local delivery`);
|
|
662
|
+
}
|
|
663
|
+
zones[index] = _extends({}, zone, change.postcodes ? {
|
|
664
|
+
postcodes: change.postcodes
|
|
665
|
+
} : {}, change.feeCents !== undefined ? {
|
|
666
|
+
feeCents: change.feeCents
|
|
667
|
+
} : {}, change.minimumCents !== undefined ? {
|
|
668
|
+
minimumCents: change.minimumCents
|
|
669
|
+
} : {}, change.freeOverCents !== undefined ? {
|
|
670
|
+
freeOverCents: change.freeOverCents
|
|
671
|
+
} : {});
|
|
672
|
+
} else {
|
|
673
|
+
var _change_postcodes;
|
|
674
|
+
if (!((_change_postcodes = change.postcodes) == null ? void 0 : _change_postcodes.length)) return fail(400, `Say which postal codes the "${change.zoneName}" zone delivers to`);
|
|
675
|
+
if (change.feeCents === undefined) return fail(400, `Say what delivery to "${change.zoneName}" costs (0 for free)`);
|
|
676
|
+
if (zones.length >= LOCAL_DELIVERY_ZONES_MAX) return fail(409, `Local delivery holds at most ${LOCAL_DELIVERY_ZONES_MAX} zones`);
|
|
677
|
+
zones.push(_extends({
|
|
678
|
+
id: ids.zoneId,
|
|
679
|
+
name: change.zoneName,
|
|
680
|
+
kind: 'postcode',
|
|
681
|
+
postcodes: change.postcodes,
|
|
682
|
+
feeCents: change.feeCents
|
|
683
|
+
}, change.minimumCents !== undefined ? {
|
|
684
|
+
minimumCents: change.minimumCents
|
|
685
|
+
} : {}, change.freeOverCents !== undefined ? {
|
|
686
|
+
freeOverCents: change.freeOverCents
|
|
687
|
+
} : {}));
|
|
688
|
+
}
|
|
689
|
+
after.zones = zones;
|
|
690
|
+
}
|
|
691
|
+
const normalized = normalizeLocalDeliverySettings(after);
|
|
692
|
+
const problems = localDeliveryProblems(normalized);
|
|
693
|
+
if (problems.length) {
|
|
694
|
+
return fail(400, `Local delivery can't be switched on yet: ${problems.join(' ')}`);
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
ok: true,
|
|
698
|
+
write: sameSetting(before, normalized) ? null : {
|
|
699
|
+
target: 'settings',
|
|
700
|
+
field: 'localDelivery',
|
|
701
|
+
value: normalized
|
|
702
|
+
},
|
|
703
|
+
summary: normalized.enabled ? `The store delivers locally to ${((_normalized_zones = normalized.zones) != null ? _normalized_zones : []).length} zone${((_normalized_zones1 = normalized.zones) != null ? _normalized_zones1 : []).length === 1 ? '' : 's'}.` : 'Local delivery is off.',
|
|
704
|
+
warnings: normalized.enabled && !normalized.locationId ? [
|
|
705
|
+
'No location is set for delivery orders to leave from.'
|
|
706
|
+
] : []
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const TAX_MODE_WORDS = {
|
|
710
|
+
manual: 'manual rates',
|
|
711
|
+
stripe: 'automatic calculation',
|
|
712
|
+
none: 'not collecting sales tax'
|
|
713
|
+
};
|
|
714
|
+
function applyTax(state, change) {
|
|
715
|
+
var _state_settings_tax, _after_rates, _TAX_MODE_WORDS_String;
|
|
716
|
+
const before = (_state_settings_tax = state.settings['tax']) != null ? _state_settings_tax : {};
|
|
717
|
+
const after = _extends({}, before);
|
|
718
|
+
const settingRate = change.ratePct !== undefined;
|
|
719
|
+
if (change.mode) after.mode = change.mode;
|
|
720
|
+
else if (settingRate && after.mode === undefined) after.mode = 'manual';
|
|
721
|
+
if (settingRate && after.mode !== 'manual') {
|
|
722
|
+
var _TAX_MODE_WORDS_String1;
|
|
723
|
+
return fail(409, `This store uses ${(_TAX_MODE_WORDS_String1 = TAX_MODE_WORDS[String(after.mode)]) != null ? _TAX_MODE_WORDS_String1 : 'another tax mode'}, so a manual rate would not apply. Ask to switch to manual rates as well.`);
|
|
724
|
+
}
|
|
725
|
+
if (change.pricesIncludeTax !== undefined) after.pricesIncludeTax = change.pricesIncludeTax;
|
|
726
|
+
if (change.originCountry) {
|
|
727
|
+
after.origin = _extends({
|
|
728
|
+
country: change.originCountry
|
|
729
|
+
}, change.originState ? {
|
|
730
|
+
state: change.originState
|
|
731
|
+
} : {});
|
|
732
|
+
}
|
|
733
|
+
if (change.rateCountry) {
|
|
734
|
+
var _before_rates;
|
|
735
|
+
const rates = [
|
|
736
|
+
...(_before_rates = before.rates) != null ? _before_rates : []
|
|
737
|
+
];
|
|
738
|
+
const index = rates.findIndex((rate)=>{
|
|
739
|
+
var _rate_country, _rate_state, _change_rateState;
|
|
740
|
+
return String((_rate_country = rate.country) != null ? _rate_country : '').toUpperCase() === change.rateCountry && String((_rate_state = rate.state) != null ? _rate_state : '').toUpperCase() === ((_change_rateState = change.rateState) != null ? _change_rateState : '');
|
|
741
|
+
});
|
|
742
|
+
if (change.removeRate) {
|
|
743
|
+
if (index >= 0) rates.splice(index, 1);
|
|
744
|
+
} else {
|
|
745
|
+
const rate = _extends({
|
|
746
|
+
country: change.rateCountry
|
|
747
|
+
}, change.rateState ? {
|
|
748
|
+
state: change.rateState
|
|
749
|
+
} : {}, {
|
|
750
|
+
pct: change.ratePct
|
|
751
|
+
}, change.rateLabel ? {
|
|
752
|
+
label: change.rateLabel
|
|
753
|
+
} : index >= 0 && rates[index].label ? {
|
|
754
|
+
label: rates[index].label
|
|
755
|
+
} : {});
|
|
756
|
+
if (index >= 0) rates[index] = rate;
|
|
757
|
+
else rates.push(rate);
|
|
758
|
+
}
|
|
759
|
+
after.rates = rates;
|
|
760
|
+
}
|
|
761
|
+
// Manual mode with no origin refuses every sale (AGL-2145): never leave a store there.
|
|
762
|
+
if (storefrontTaxMisconfiguration(after)) {
|
|
763
|
+
return fail(400, 'Manual tax rates need the store’s address: say which country (and state) the store sells from.');
|
|
764
|
+
}
|
|
765
|
+
const warnings = [];
|
|
766
|
+
if (after.mode === 'manual' && !((_after_rates = after.rates) != null ? _after_rates : []).length) {
|
|
767
|
+
warnings.push('No tax rate is set yet, so orders are charged no sales tax.');
|
|
768
|
+
}
|
|
769
|
+
if (after.mode === 'stripe') {
|
|
770
|
+
warnings.push('Automatic tax is calculated per buyer location at checkout; the register cannot calculate it for in-person sales.');
|
|
771
|
+
}
|
|
772
|
+
const mode = (_TAX_MODE_WORDS_String = TAX_MODE_WORDS[String(after.mode)]) != null ? _TAX_MODE_WORDS_String : 'undecided';
|
|
773
|
+
const rateLine = settingRate ? ` ${change.ratePct}% for ${change.rateCountry}${change.rateState ? `-${change.rateState}` : ''}.` : '';
|
|
774
|
+
return {
|
|
775
|
+
ok: true,
|
|
776
|
+
write: sameSetting(before, after) ? null : {
|
|
777
|
+
target: 'settings',
|
|
778
|
+
field: 'tax',
|
|
779
|
+
value: after
|
|
780
|
+
},
|
|
781
|
+
summary: `Sales tax: ${mode}.${rateLine}`,
|
|
782
|
+
warnings
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
/** The settings after a change, what to write, and what the person is told. */ export function applyStoreSetupChange(state, change, ids) {
|
|
786
|
+
switch(change.section){
|
|
787
|
+
case 'shipping':
|
|
788
|
+
return applyShipping(state, change, ids);
|
|
789
|
+
case 'pickup':
|
|
790
|
+
return applyPickup(state, change);
|
|
791
|
+
case 'local-delivery':
|
|
792
|
+
return applyLocalDelivery(state, change, ids);
|
|
793
|
+
case 'tax':
|
|
794
|
+
return applyTax(state, change);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/** What a reader is told about a store's setup, for the writer's `read`. */ export function storeSetupFacts(settings) {
|
|
798
|
+
var _settings_shipping, _settings_tax, _shipping_zones, _shipping_rates, _tax_mode, _tax_rates;
|
|
799
|
+
const shipping = (_settings_shipping = settings['shipping']) != null ? _settings_shipping : {};
|
|
800
|
+
const tax = (_settings_tax = settings['tax']) != null ? _settings_tax : {};
|
|
801
|
+
const delivery = normalizeLocalDeliverySettings(settings['localDelivery']);
|
|
802
|
+
return {
|
|
803
|
+
shippingZones: ((_shipping_zones = shipping.zones) != null ? _shipping_zones : []).length,
|
|
804
|
+
shippingRates: ((_shipping_rates = shipping.rates) != null ? _shipping_rates : []).length,
|
|
805
|
+
localPickup: Boolean(shipping.localPickup),
|
|
806
|
+
localDelivery: Boolean(delivery.enabled),
|
|
807
|
+
taxMode: (_tax_mode = tax.mode) != null ? _tax_mode : null,
|
|
808
|
+
taxRates: ((_tax_rates = tax.rates) != null ? _tax_rates : []).length,
|
|
809
|
+
live: true
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
//# sourceMappingURL=store-setup-content.js.map
|