@sixtyninecommerce/admin 0.11.0 → 0.12.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/README.md +1501 -23
- package/dist/auth/index.d.ts +7 -1
- package/dist/auth/index.js +8 -1
- package/dist/catalog/documents.d.ts +142 -0
- package/dist/catalog/documents.js +114 -0
- package/dist/catalog/index.d.ts +48 -7
- package/dist/catalog/index.js +6 -1
- package/dist/catalog/metadata.d.ts +244 -0
- package/dist/catalog/metadata.js +220 -0
- package/dist/catalog/properties.d.ts +38 -0
- package/dist/catalog/properties.js +52 -1
- package/dist/catalog/recommendations.d.ts +33 -6
- package/dist/catalog/recommendations.js +94 -2
- package/dist/catalog/reviews.d.ts +17 -4
- package/dist/catalog/reviews.js +7 -0
- package/dist/catalog/variants.d.ts +60 -0
- package/dist/catalog/variants.js +39 -0
- package/dist/catalog/visibility.d.ts +18 -2
- package/dist/client.d.ts +29 -3
- package/dist/client.js +19 -8
- package/dist/content/assets.d.ts +3 -4
- package/dist/content/forms.d.ts +332 -1
- package/dist/content/forms.js +140 -4
- package/dist/content/items.d.ts +31 -8
- package/dist/content/items.js +1 -1
- package/dist/content/menus.d.ts +254 -0
- package/dist/content/menus.js +70 -0
- package/dist/content/strings.d.ts +137 -0
- package/dist/content/strings.js +82 -0
- package/dist/customers/delivery-terms.d.ts +44 -0
- package/dist/customers/delivery-terms.js +32 -0
- package/dist/customers/index.d.ts +15 -0
- package/dist/customers/index.js +4 -0
- package/dist/documents/emails.d.ts +150 -0
- package/dist/documents/emails.js +220 -0
- package/dist/documents/index.d.ts +13 -0
- package/dist/documents/index.js +2 -0
- package/dist/fulfillments/index.d.ts +119 -1
- package/dist/fulfillments/index.js +124 -4
- package/dist/index.d.ts +43 -23
- package/dist/index.js +11 -5
- package/dist/integrations/exact-online-types.d.ts +542 -0
- package/dist/integrations/exact-online.d.ts +541 -5
- package/dist/integrations/exact-online.js +714 -1
- package/dist/integrations/index.d.ts +23 -8
- package/dist/integrations/index.js +9 -4
- package/dist/integrations/pipeline-discoveries.d.ts +65 -0
- package/dist/integrations/pipeline-discoveries.js +65 -0
- package/dist/integrations/pipeline-runs.d.ts +119 -0
- package/dist/integrations/pipeline-runs.js +87 -0
- package/dist/integrations/pipelines.d.ts +73 -2
- package/dist/integrations/pipelines.js +36 -1
- package/dist/integrations/price-import.d.ts +223 -0
- package/dist/integrations/price-import.js +124 -0
- package/dist/integrations/types.d.ts +221 -3
- package/dist/integrations/types.js +4 -3
- package/dist/meta/index.d.ts +10 -0
- package/dist/metafields/index.d.ts +8 -1
- package/dist/metafields/index.js +10 -0
- package/dist/orders/cancel-with-money.d.ts +10 -0
- package/dist/orders/cancel-with-money.js +2 -0
- package/dist/orders/commit-edit.d.ts +16 -0
- package/dist/orders/commit-edit.js +14 -1
- package/dist/orders/deposit-policy.d.ts +3 -1
- package/dist/orders/index.d.ts +22 -11
- package/dist/orders/index.js +5 -2
- package/dist/orders/list.d.ts +26 -5
- package/dist/orders/list.js +123 -13
- package/dist/orders/manual-discounts.d.ts +13 -5
- package/dist/orders/money-view.d.ts +149 -0
- package/dist/orders/money-view.js +99 -0
- package/dist/orders/plan-edit.d.ts +34 -2
- package/dist/orders/plan-edit.js +110 -12
- package/dist/orders/surcharges.d.ts +12 -0
- package/dist/orders/surcharges.js +34 -9
- package/dist/payments/account-credit.d.ts +17 -1
- package/dist/payments/account-credit.js +25 -2
- package/dist/payments/credit-noted-fields.d.ts +8 -1
- package/dist/payments/credit-noted-fields.js +34 -2
- package/dist/payments/customer-consent.d.ts +30 -0
- package/dist/payments/customer-consent.js +49 -0
- package/dist/payments/index.d.ts +89 -14
- package/dist/payments/index.js +22 -2
- package/dist/payments/requests.d.ts +6 -3
- package/dist/payments/requests.js +1 -0
- package/dist/pickups/index.d.ts +5 -3
- package/dist/pickups/index.js +5 -2
- package/dist/platform/email-gateways.d.ts +165 -0
- package/dist/platform/email-gateways.js +78 -0
- package/dist/platform/tax.d.ts +11 -3
- package/dist/platform/tax.js +30 -0
- package/dist/pos/exchanges.d.ts +6 -1
- package/dist/pos/index.d.ts +229 -10
- package/dist/pos/index.js +113 -4
- package/dist/pos/offline-sales.d.ts +44 -3
- package/dist/pos/offline-sales.js +57 -3
- package/dist/pos/returns.d.ts +28 -1
- package/dist/pos/returns.js +36 -6
- package/dist/pricing/price-lists.d.ts +3 -1
- package/dist/purchasing/alerts.d.ts +19 -4
- package/dist/purchasing/alerts.js +29 -3
- package/dist/purchasing/backorders.d.ts +61 -0
- package/dist/purchasing/backorders.js +92 -0
- package/dist/purchasing/index.d.ts +21 -3
- package/dist/purchasing/index.js +8 -0
- package/dist/purchasing/links.d.ts +263 -0
- package/dist/purchasing/links.js +192 -0
- package/dist/purchasing/purchase-orders.d.ts +4 -1
- package/dist/purchasing/receipts.d.ts +396 -0
- package/dist/purchasing/receipts.js +287 -0
- package/dist/purchasing/returns.d.ts +273 -0
- package/dist/purchasing/returns.js +229 -0
- package/dist/quotes/index.d.ts +20 -3
- package/dist/returns/index.d.ts +62 -1
- package/dist/returns/index.js +61 -0
- package/dist/seo/config.d.ts +128 -0
- package/dist/seo/config.js +95 -0
- package/dist/seo/redirects.d.ts +125 -0
- package/dist/seo/redirects.js +60 -1
- package/dist/seo/settings.d.ts +147 -0
- package/dist/seo/settings.js +209 -0
- package/dist/shipping/index.d.ts +6 -0
- package/dist/shipping/index.js +2 -0
- package/dist/shipping/methods.d.ts +8 -1
- package/dist/shipping/myparcel.d.ts +415 -1
- package/dist/shipping/myparcel.js +291 -3
- package/dist/shipping/rate-tables.d.ts +149 -4
- package/dist/shipping/rate-tables.js +229 -23
- package/dist/shipping/ship-to-countries.d.ts +75 -0
- package/dist/shipping/ship-to-countries.js +47 -0
- package/dist/stock/by-location.d.ts +86 -0
- package/dist/stock/by-location.js +94 -0
- package/dist/stock/index.d.ts +8 -1
- package/dist/stock/index.js +4 -0
- package/dist/stock/settings.d.ts +106 -0
- package/dist/stock/settings.js +103 -0
- package/dist/stock/transfers.d.ts +171 -10
- package/dist/stock/transfers.js +82 -4
- package/dist/stored-value/index.d.ts +98 -7
- package/dist/stored-value/index.js +71 -4
- package/dist/tax/display-policy.d.ts +105 -0
- package/dist/tax/display-policy.js +84 -0
- package/dist/tax/index.d.ts +3 -0
- package/dist/tax/index.js +2 -0
- package/dist/tenant/email-templates.d.ts +222 -1
- package/dist/tenant/email-templates.js +227 -10
- package/package.json +2 -2
package/dist/seo/redirects.d.ts
CHANGED
|
@@ -18,10 +18,135 @@ export interface RedirectCreateInput {
|
|
|
18
18
|
statusCode?: number;
|
|
19
19
|
}
|
|
20
20
|
export type RedirectUpdateInput = Partial<RedirectCreateInput>;
|
|
21
|
+
/**
|
|
22
|
+
* The most rows one {@link RedirectsModule.import} takes. Mirrors `ArrayMaxSize` on the plugins
|
|
23
|
+
* service's `ImportRedirectsDto` (`services/plugins/src/redirects/dto/import-redirects.dto.ts`),
|
|
24
|
+
* pinned by `__tests__/redirects.spec.ts`. Change both sides together.
|
|
25
|
+
*/
|
|
26
|
+
export declare const REDIRECT_IMPORT_MAX_ROWS = 5000;
|
|
27
|
+
/**
|
|
28
|
+
* How long an import may take before the call gives up, unless the caller says otherwise: the
|
|
29
|
+
* service saves the rows one by one, and a full batch takes far longer than a single write.
|
|
30
|
+
*/
|
|
31
|
+
export declare const REDIRECT_IMPORT_TIMEOUT_MS = 60000;
|
|
32
|
+
/**
|
|
33
|
+
* One row of {@link RedirectsModule.import}. Not checked before sending: the service judges each
|
|
34
|
+
* row on its own and reports a bad one in {@link RedirectImportResult.rejected}, so a malformed
|
|
35
|
+
* row never costs the rest of the batch.
|
|
36
|
+
*/
|
|
37
|
+
export interface RedirectImportRow {
|
|
38
|
+
/** One exact path, e.g. `/product/old-slug`. */
|
|
39
|
+
oldPath: string;
|
|
40
|
+
/** A path, or an absolute `https://` URL. */
|
|
41
|
+
newPath: string;
|
|
42
|
+
/** 301 (the default), 302, 303, 307 or 308. */
|
|
43
|
+
statusCode?: number;
|
|
44
|
+
}
|
|
45
|
+
/** A row the service refused, and why. The other rows still landed. */
|
|
46
|
+
export interface RedirectImportRejection {
|
|
47
|
+
/** The row's position in the array sent, from 0. */
|
|
48
|
+
index: number;
|
|
49
|
+
/** The row's source path as sent. */
|
|
50
|
+
oldPath: string;
|
|
51
|
+
/**
|
|
52
|
+
* The service's sentence for the person fixing the file: a source that is not one exact path,
|
|
53
|
+
* a destination that is neither a path nor an `https://` URL, a status code outside the five,
|
|
54
|
+
* a source that redirects to itself, or a row that would close a redirect loop or a chain past
|
|
55
|
+
* the hop limit (with the rows already stored or with an earlier row of the same batch). There
|
|
56
|
+
* is no code per row yet.
|
|
57
|
+
*/
|
|
58
|
+
message: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What an import did. A row whose source the store already redirects UPDATES that redirect
|
|
62
|
+
* (same source, so never a duplicate); any other accepted row creates one.
|
|
63
|
+
*/
|
|
64
|
+
export interface RedirectImportResult {
|
|
65
|
+
created: number;
|
|
66
|
+
updated: number;
|
|
67
|
+
/** Empty when every row landed. */
|
|
68
|
+
rejected: RedirectImportRejection[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `admin.seo.redirects` — the ACTIVE store's URL redirects, on the plugins service. Storefronts read
|
|
72
|
+
* the same rows live from the public redirect map (`@sixtyninecommerce/client/next`'s
|
|
73
|
+
* `createRedirectMiddleware`), so a write here changes what the store's old URLs answer within a
|
|
74
|
+
* few minutes, with no deployment.
|
|
75
|
+
*
|
|
76
|
+
* Every method needs `UpdateChannel` (`FORBIDDEN` → `PermissionDeniedError`, 403, otherwise). The
|
|
77
|
+
* writes that set up a redirect (`create`, `update`, `import`) are refused in an organization's
|
|
78
|
+
* workspace with `ORGANIZATION_WORKSPACE_NOT_A_STORE` (`BusinessRuleError`, 422): a workspace has
|
|
79
|
+
* no storefront. Removing one is never refused there.
|
|
80
|
+
*/
|
|
21
81
|
export interface RedirectsModule {
|
|
82
|
+
/**
|
|
83
|
+
* The store's redirects, newest first, a page at a time (`limit` 1 to 200; 50 by default).
|
|
84
|
+
* Iterate the page (`for await`) to walk every page.
|
|
85
|
+
*
|
|
86
|
+
* @effect read
|
|
87
|
+
* @permission UpdateChannel
|
|
88
|
+
*/
|
|
22
89
|
list(options?: ListOptions): Promise<Page<Redirect>>;
|
|
90
|
+
/**
|
|
91
|
+
* Add one redirect. The service applies the same path rules as `import`: a malformed path, a
|
|
92
|
+
* self-redirect or a status code outside the five is `ValidationError` (400); a source the store
|
|
93
|
+
* already redirects, a loop, or a chain past the service's hop limit is `ConflictError` (409).
|
|
94
|
+
* The idempotency key is honoured: a repeat answers the first response.
|
|
95
|
+
*
|
|
96
|
+
* @effect write
|
|
97
|
+
* @permission UpdateChannel
|
|
98
|
+
*/
|
|
23
99
|
create(input: RedirectCreateInput, options?: Call): Promise<Redirect>;
|
|
100
|
+
/**
|
|
101
|
+
* Change the fields `input` names; at least one. The same rules as `create`; an unknown id is
|
|
102
|
+
* `NotFoundError` (404).
|
|
103
|
+
*
|
|
104
|
+
* @effect write
|
|
105
|
+
* @permission UpdateChannel
|
|
106
|
+
*/
|
|
24
107
|
update(id: string, input: RedirectUpdateInput, options?: Call): Promise<Redirect>;
|
|
108
|
+
/**
|
|
109
|
+
* Remove one redirect: the old URL stops redirecting once the storefront's copy of the map
|
|
110
|
+
* expires. An unknown id is `NotFoundError` (404).
|
|
111
|
+
*
|
|
112
|
+
* @effect destructive
|
|
113
|
+
* @permission UpdateChannel
|
|
114
|
+
*/
|
|
25
115
|
delete(id: string, options?: Call): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Add or update up to {@link REDIRECT_IMPORT_MAX_ROWS} redirects in one call, matched on the
|
|
118
|
+
* source path (a source the store already redirects is updated, never duplicated). Each row is
|
|
119
|
+
* judged on its own: a bad one is answered in `rejected` by its index and the rest still land.
|
|
120
|
+
* Rows are applied in order, so a later row may build on an earlier one, and a loop split
|
|
121
|
+
* across two rows is refused on the second.
|
|
122
|
+
*
|
|
123
|
+
* Not atomic and not deduplicated by an idempotency key, but safe to repeat: sending the same
|
|
124
|
+
* rows again updates them to what they already are.
|
|
125
|
+
*
|
|
126
|
+
* **Coded refusals:**
|
|
127
|
+
* - `REDIRECT_IMPORT_INVALID` → `ValidationError` (400), before any request: `rows` is not an
|
|
128
|
+
* array of 1 to {@link REDIRECT_IMPORT_MAX_ROWS} entries. Send a larger file in chunks.
|
|
129
|
+
* - `ORGANIZATION_WORKSPACE_NOT_A_STORE` → `BusinessRuleError` (422): the active store is an
|
|
130
|
+
* organization's workspace.
|
|
131
|
+
* - `FORBIDDEN` → `PermissionDeniedError` (403): the credential lacks `UpdateChannel`.
|
|
132
|
+
*
|
|
133
|
+
* Waits up to {@link REDIRECT_IMPORT_TIMEOUT_MS}, longer than the client's default deadline. A
|
|
134
|
+
* timed-out import may still have landed (the service keeps saving): list the redirects, or
|
|
135
|
+
* send the same rows again, which is safe.
|
|
136
|
+
*
|
|
137
|
+
* @effect write
|
|
138
|
+
* @permission UpdateChannel
|
|
139
|
+
*/
|
|
140
|
+
import(rows: readonly RedirectImportRow[], options?: Call): Promise<RedirectImportResult>;
|
|
141
|
+
/**
|
|
142
|
+
* The store's redirects as two Nginx `map` blocks (text: `$redirect_uri` and
|
|
143
|
+
* `$redirect_status`, keyed on `$uri`), for a site that is not on the storefront SDK: include
|
|
144
|
+
* it in the server config and redirect on the mapped values. One entry per source path, those
|
|
145
|
+
* a storefront cannot serve included.
|
|
146
|
+
*
|
|
147
|
+
* @effect read
|
|
148
|
+
* @permission UpdateChannel
|
|
149
|
+
*/
|
|
150
|
+
exportNginx(options?: Call): Promise<string>;
|
|
26
151
|
}
|
|
27
152
|
export declare function createRedirectsModule(transport: Transport): RedirectsModule;
|
package/dist/seo/redirects.js
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
import { createPage, readRestPage } from '@sixtyninecommerce/core';
|
|
1
|
+
import { createPage, readRestPage, ValidationError, } from '@sixtyninecommerce/core';
|
|
2
|
+
/**
|
|
3
|
+
* The most rows one {@link RedirectsModule.import} takes. Mirrors `ArrayMaxSize` on the plugins
|
|
4
|
+
* service's `ImportRedirectsDto` (`services/plugins/src/redirects/dto/import-redirects.dto.ts`),
|
|
5
|
+
* pinned by `__tests__/redirects.spec.ts`. Change both sides together.
|
|
6
|
+
*/
|
|
7
|
+
export const REDIRECT_IMPORT_MAX_ROWS = 5000;
|
|
8
|
+
/**
|
|
9
|
+
* How long an import may take before the call gives up, unless the caller says otherwise: the
|
|
10
|
+
* service saves the rows one by one, and a full batch takes far longer than a single write.
|
|
11
|
+
*/
|
|
12
|
+
export const REDIRECT_IMPORT_TIMEOUT_MS = 60_000;
|
|
13
|
+
const IMPORT_INVALID = 'REDIRECT_IMPORT_INVALID';
|
|
14
|
+
/** The import's answer, read defensively: an older service may leave `rejected` out. */
|
|
15
|
+
function importResultOf(body) {
|
|
16
|
+
const record = (typeof body === 'object' && body !== null ? body : {});
|
|
17
|
+
const count = (value) => (typeof value === 'number' ? value : 0);
|
|
18
|
+
const rejected = Array.isArray(record.rejected)
|
|
19
|
+
? record.rejected
|
|
20
|
+
: [];
|
|
21
|
+
return { created: count(record.created), updated: count(record.updated), rejected };
|
|
22
|
+
}
|
|
2
23
|
export function createRedirectsModule(transport) {
|
|
3
24
|
async function list(options) {
|
|
4
25
|
// `GET /redirects` answers with a bare array unless the request OPTS IN.
|
|
@@ -33,5 +54,43 @@ export function createRedirectsModule(transport) {
|
|
|
33
54
|
// discards the body rather than pretending it is a Redirect.
|
|
34
55
|
await transport.rest('plugins', { method: 'DELETE', path: `/redirects/${encodeURIComponent(id)}`, expect: 'void' }, options);
|
|
35
56
|
},
|
|
57
|
+
async import(rows, options) {
|
|
58
|
+
if (!Array.isArray(rows) || rows.length === 0 || rows.length > REDIRECT_IMPORT_MAX_ROWS) {
|
|
59
|
+
throw new ValidationError({
|
|
60
|
+
code: IMPORT_INVALID,
|
|
61
|
+
message: `Send 1 to ${String(REDIRECT_IMPORT_MAX_ROWS)} redirects per import; split a larger file into several calls.`,
|
|
62
|
+
status: 400,
|
|
63
|
+
service: 'plugins',
|
|
64
|
+
details: [
|
|
65
|
+
{
|
|
66
|
+
field: 'rows',
|
|
67
|
+
code: IMPORT_INVALID,
|
|
68
|
+
message: Array.isArray(rows)
|
|
69
|
+
? `${String(rows.length)} rows sent.`
|
|
70
|
+
: 'rows is not an array.',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const body = await transport.rest('plugins', {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
path: '/redirects/import',
|
|
78
|
+
// Only the three row fields go out: the service's DTO refuses any other key, which
|
|
79
|
+
// would fail the whole batch rather than the one row.
|
|
80
|
+
body: {
|
|
81
|
+
redirects: rows.map((row) => ({
|
|
82
|
+
oldPath: row.oldPath,
|
|
83
|
+
newPath: row.newPath,
|
|
84
|
+
...(row.statusCode === undefined ? {} : { statusCode: row.statusCode }),
|
|
85
|
+
})),
|
|
86
|
+
},
|
|
87
|
+
}, { timeoutMs: REDIRECT_IMPORT_TIMEOUT_MS, ...options });
|
|
88
|
+
return importResultOf(body);
|
|
89
|
+
},
|
|
90
|
+
async exportNginx(options) {
|
|
91
|
+
// A text body, never JSON: `blob` keeps the transport from parsing or case-converting it.
|
|
92
|
+
const answer = await transport.rest('plugins', { method: 'GET', path: '/redirects/nginx', expect: 'blob' }, options);
|
|
93
|
+
return answer.blob.text();
|
|
94
|
+
},
|
|
36
95
|
};
|
|
37
96
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { type Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { SettingsSourceKind } from '../tenant/settings-sources.js';
|
|
3
|
+
import type { Call } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* How a language shows in storefront URLs. Mirrors the meta catalog's `URL_LOCALE_STRATEGIES`
|
|
6
|
+
* (`packages/shared/src/meta-catalog/plugin-config-schemas.ts`), which the engine enforces,
|
|
7
|
+
* pinned by `__tests__/seo-settings.spec.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const URL_LOCALE_STRATEGIES: readonly ["path-prefix", "subdomain", "none"];
|
|
10
|
+
export type UrlLocaleStrategy = (typeof URL_LOCALE_STRATEGIES)[number];
|
|
11
|
+
/** What an AI crawler may do with the store's pages. */
|
|
12
|
+
export type AiBotDirective = 'allow' | 'deny';
|
|
13
|
+
/**
|
|
14
|
+
* The store's SEO defaults: the `settings.seo` group's channel half, which a member store may take
|
|
15
|
+
* from its organization.
|
|
16
|
+
*/
|
|
17
|
+
export interface SeoDefaults {
|
|
18
|
+
/** Without the `@`; storefronts emit it as `twitter:site`. Null for none. */
|
|
19
|
+
twitterHandle: string | null;
|
|
20
|
+
/** The asset shared as the OpenGraph image when a product has none; null for none. */
|
|
21
|
+
defaultOgImageId: string | null;
|
|
22
|
+
/** AI crawler → `allow` or `deny`. A crawler left out follows the storefront's own default. */
|
|
23
|
+
aiBotDefaults: Record<string, AiBotDirective>;
|
|
24
|
+
/** The storefront advertises its AI-friendly endpoints at `/llms.txt`. */
|
|
25
|
+
llmsTxtEnabled: boolean;
|
|
26
|
+
/** Plain text merged into the store's `robots.txt`; null for none. */
|
|
27
|
+
robotsTxtCustom: string | null;
|
|
28
|
+
}
|
|
29
|
+
/** Where the SEO defaults storefronts get come from (`settings.seo`). */
|
|
30
|
+
export interface SeoDefaultsSource {
|
|
31
|
+
/** `channel`: the store's own defaults apply; `organization`: its organization's. */
|
|
32
|
+
source: SettingsSourceKind | (string & {});
|
|
33
|
+
/**
|
|
34
|
+
* The organization locks the group: {@link SeoSettingsModule.set} refuses a changed default with
|
|
35
|
+
* `MANAGED_BY_ORGANIZATION`, SuperAdmins included.
|
|
36
|
+
*/
|
|
37
|
+
locked: boolean;
|
|
38
|
+
/** The organization shares the group without locking it, so the store may override it. */
|
|
39
|
+
inheritable: boolean;
|
|
40
|
+
/** The store keeps its own defaults although the organization shares the group. */
|
|
41
|
+
overridden: boolean;
|
|
42
|
+
/** The store's organization; null for a store outside any organization. */
|
|
43
|
+
organizationName: string | null;
|
|
44
|
+
/** The defaults storefronts get: the store's own while `source` is `channel`, else the organization's. */
|
|
45
|
+
effective: SeoDefaults;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The ACTIVE store's SEO settings as its channel stores them. The top-level defaults are the
|
|
49
|
+
* store's OWN values, which {@link SeoSettingsModule.set} writes; while the store takes
|
|
50
|
+
* `settings.seo` from its organization, storefronts get `defaultsSource.effective` instead.
|
|
51
|
+
*/
|
|
52
|
+
export interface SeoSettings extends SeoDefaults {
|
|
53
|
+
/** The store these are the settings of (its channel id). */
|
|
54
|
+
channelId: string;
|
|
55
|
+
defaultLanguageCode: string;
|
|
56
|
+
/** The store's languages, its default included. */
|
|
57
|
+
availableLanguageCodes: string[];
|
|
58
|
+
/**
|
|
59
|
+
* The storefront's address, the base of canonical, hreflang and sitemap URLs; null when none.
|
|
60
|
+
* Always the store's own. While the store has a default web head, the head's address is copied
|
|
61
|
+
* over it on every head change.
|
|
62
|
+
*/
|
|
63
|
+
storefrontUrl: string | null;
|
|
64
|
+
/**
|
|
65
|
+
* How a language shows in storefront URLs; `path-prefix` when the store never chose. A string
|
|
66
|
+
* the engine accepts that this SDK does not name yet passes through. Always the store's own.
|
|
67
|
+
*/
|
|
68
|
+
urlLocaleStrategy: UrlLocaleStrategy | (string & {});
|
|
69
|
+
/** The default language carries a prefix too. Always the store's own. */
|
|
70
|
+
urlLocalePrefixDefault: boolean;
|
|
71
|
+
defaultsSource: SeoDefaultsSource;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A patch of {@link SeoSettings}: a key left out is unchanged, `null` clears it. At least one key.
|
|
75
|
+
* The read-only keys `get` answers (`channelId`, the languages, `defaultsSource`) are ignored, so
|
|
76
|
+
* `set({ ...settings, urlLocaleStrategy: 'subdomain' })` works.
|
|
77
|
+
*/
|
|
78
|
+
export interface SeoSettingsPatch {
|
|
79
|
+
/** At most 255 characters; not checked to be a URL. */
|
|
80
|
+
storefrontUrl?: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* One of {@link URL_LOCALE_STRATEGIES}, or null for the default (`path-prefix`). Typed as any
|
|
83
|
+
* string so a strategy `get` answered (one this SDK may not name yet) can be sent back; the
|
|
84
|
+
* engine refuses one it does not know (`USER_INPUT_ERROR`).
|
|
85
|
+
*/
|
|
86
|
+
urlLocaleStrategy?: UrlLocaleStrategy | (string & {}) | null;
|
|
87
|
+
urlLocalePrefixDefault?: boolean | null;
|
|
88
|
+
/** At most 50 characters, without the `@`. */
|
|
89
|
+
twitterHandle?: string | null;
|
|
90
|
+
defaultOgImageId?: string | null;
|
|
91
|
+
/** The whole map (it replaces the stored one), as an object: the SDK encodes it. */
|
|
92
|
+
aiBotDefaults?: Record<string, AiBotDirective> | null;
|
|
93
|
+
llmsTxtEnabled?: boolean | null;
|
|
94
|
+
robotsTxtCustom?: string | null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `admin.seo.settings`: the ACTIVE store's SEO channel settings (Settings → SEO): the storefront
|
|
98
|
+
* URL and how languages show in its URLs, and the SEO defaults (share card, AI crawlers,
|
|
99
|
+
* `llms.txt`, `robots.txt`). Stored as Channel custom fields; written through core `updateChannel`
|
|
100
|
+
* (typed per domain, ruling G80).
|
|
101
|
+
*
|
|
102
|
+
* The other half of the page, the SEO automation (automatic redirects, IndexNow, the audit
|
|
103
|
+
* schedule), lives on the plugins service: `admin.seo.config`.
|
|
104
|
+
*/
|
|
105
|
+
export interface SeoSettingsModule {
|
|
106
|
+
/**
|
|
107
|
+
* The settings, and where the defaults storefronts get come from. Reading needs an
|
|
108
|
+
* administrator of the store with any permission beyond `Authenticated`.
|
|
109
|
+
*
|
|
110
|
+
* @effect read
|
|
111
|
+
*/
|
|
112
|
+
get(options?: Call): Promise<SeoSettings>;
|
|
113
|
+
/**
|
|
114
|
+
* Change the settings `patch` names, and answer them all as the engine now stores them. The
|
|
115
|
+
* storefront picks the change up on its next read of the store's settings.
|
|
116
|
+
*
|
|
117
|
+
* **Coded refusals:**
|
|
118
|
+
* - `SEO_SETTINGS_INVALID` → `ValidationError` (400), before any request: an empty patch, a
|
|
119
|
+
* key that is not an SEO setting, or an `aiBotDefaults` that is not an object (or null). One
|
|
120
|
+
* `details[]` entry per key, `field` naming it.
|
|
121
|
+
* - `CHANNEL_SETTINGS_INVALID` → `ValidationError` (400), from the engine: an `aiBotDefaults`
|
|
122
|
+
* the platform's schema refuses (a directive other than `allow` or `deny`).
|
|
123
|
+
* `details[].field` is `customFields.aiBotDefaults`.
|
|
124
|
+
* - `USER_INPUT_ERROR` → `ValidationError` (400): a `urlLocaleStrategy` outside
|
|
125
|
+
* {@link URL_LOCALE_STRATEGIES}.
|
|
126
|
+
* - `MANAGED_BY_ORGANIZATION` → `BusinessRuleError` (422): a changed default while the
|
|
127
|
+
* organization locks `settings.seo`.
|
|
128
|
+
* - `ORGANIZATION_SETTINGS_INHERITED` → `BusinessRuleError` (422): a changed default while the
|
|
129
|
+
* store inherits the group; `tenant.settingsSources.override('settings.seo')` it first.
|
|
130
|
+
* Sending the stored value back unchanged is accepted and changes nothing.
|
|
131
|
+
* - `ORGANIZATION_WORKSPACE_NOT_A_STORE` → `BusinessRuleError` (422): a storefront URL for an
|
|
132
|
+
* organization's workspace, which has no storefront.
|
|
133
|
+
* - `FORBIDDEN` → `PermissionDeniedError` (403): the credential lacks `UpdateChannel`.
|
|
134
|
+
*
|
|
135
|
+
* @effect write
|
|
136
|
+
* @permission UpdateChannel
|
|
137
|
+
*/
|
|
138
|
+
set(patch: SeoSettingsPatch, options?: Call): Promise<SeoSettings>;
|
|
139
|
+
}
|
|
140
|
+
export declare const SEO_SETTINGS_QUERY = "\n query SeoSettings {\n activeChannel { \n id\n defaultLanguageCode\n availableLanguageCodes\n customFields {\n storefrontUrl\n urlLocaleStrategy\n urlLocalePrefixDefault\n twitterHandle\n defaultOgImageId\n aiBotDefaults\n llmsTxtEnabled\n robotsTxtCustom\n }\n }\n channelSettingsSources { group source locked overridden inheritable organizationName values }\n }\n";
|
|
141
|
+
export declare const UPDATE_SEO_SETTINGS_MUTATION = "\n mutation UpdateSeoSettings($input: UpdateChannelInput!) {\n updateChannel(input: $input) {\n ... on Channel { \n id\n defaultLanguageCode\n availableLanguageCodes\n customFields {\n storefrontUrl\n urlLocaleStrategy\n urlLocalePrefixDefault\n twitterHandle\n defaultOgImageId\n aiBotDefaults\n llmsTxtEnabled\n robotsTxtCustom\n }\n }\n ... on ErrorResult { errorCode message }\n }\n }\n";
|
|
142
|
+
/**
|
|
143
|
+
* `aiBotDefaults` as the column stores it (JSON text) or already decoded (an organization's
|
|
144
|
+
* `values`); a directive other than `allow` or `deny` is left out, an unreadable value is none.
|
|
145
|
+
*/
|
|
146
|
+
export declare function readAiBotDefaults(raw: unknown): Record<string, AiBotDirective>;
|
|
147
|
+
export declare function createSeoSettingsModule(transport: Transport): SeoSettingsModule;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { errorFromErrorResult, isErrorResult, NotFoundError, ValidationError, } from '@sixtyninecommerce/core';
|
|
2
|
+
/**
|
|
3
|
+
* How a language shows in storefront URLs. Mirrors the meta catalog's `URL_LOCALE_STRATEGIES`
|
|
4
|
+
* (`packages/shared/src/meta-catalog/plugin-config-schemas.ts`), which the engine enforces,
|
|
5
|
+
* pinned by `__tests__/seo-settings.spec.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export const URL_LOCALE_STRATEGIES = ['path-prefix', 'subdomain', 'none'];
|
|
8
|
+
/** The settings group the defaults belong to (multi-region spec §5.3). */
|
|
9
|
+
const SEO_GROUP = 'settings.seo';
|
|
10
|
+
const CHANNEL_FIELDS = /* GraphQL */ `
|
|
11
|
+
id
|
|
12
|
+
defaultLanguageCode
|
|
13
|
+
availableLanguageCodes
|
|
14
|
+
customFields {
|
|
15
|
+
storefrontUrl
|
|
16
|
+
urlLocaleStrategy
|
|
17
|
+
urlLocalePrefixDefault
|
|
18
|
+
twitterHandle
|
|
19
|
+
defaultOgImageId
|
|
20
|
+
aiBotDefaults
|
|
21
|
+
llmsTxtEnabled
|
|
22
|
+
robotsTxtCustom
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
export const SEO_SETTINGS_QUERY = /* GraphQL */ `
|
|
26
|
+
query SeoSettings {
|
|
27
|
+
activeChannel { ${CHANNEL_FIELDS} }
|
|
28
|
+
channelSettingsSources { group source locked overridden inheritable organizationName values }
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
export const UPDATE_SEO_SETTINGS_MUTATION = /* GraphQL */ `
|
|
32
|
+
mutation UpdateSeoSettings($input: UpdateChannelInput!) {
|
|
33
|
+
updateChannel(input: $input) {
|
|
34
|
+
... on Channel { ${CHANNEL_FIELDS} }
|
|
35
|
+
... on ErrorResult { errorCode message }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
const INVALID = 'SEO_SETTINGS_INVALID';
|
|
40
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
+
const stringOrNull = (value) => (typeof value === 'string' ? value : null);
|
|
42
|
+
/**
|
|
43
|
+
* `aiBotDefaults` as the column stores it (JSON text) or already decoded (an organization's
|
|
44
|
+
* `values`); a directive other than `allow` or `deny` is left out, an unreadable value is none.
|
|
45
|
+
*/
|
|
46
|
+
export function readAiBotDefaults(raw) {
|
|
47
|
+
let parsed = raw;
|
|
48
|
+
if (typeof raw === 'string') {
|
|
49
|
+
if (raw === '')
|
|
50
|
+
return {};
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!isRecord(parsed))
|
|
59
|
+
return {};
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [bot, directive] of Object.entries(parsed)) {
|
|
62
|
+
if (directive === 'allow' || directive === 'deny')
|
|
63
|
+
out[bot] = directive;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function defaultsOf(fields) {
|
|
68
|
+
return {
|
|
69
|
+
twitterHandle: stringOrNull(fields.twitterHandle),
|
|
70
|
+
defaultOgImageId: stringOrNull(fields.defaultOgImageId),
|
|
71
|
+
aiBotDefaults: readAiBotDefaults(fields.aiBotDefaults),
|
|
72
|
+
llmsTxtEnabled: fields.llmsTxtEnabled === true,
|
|
73
|
+
robotsTxtCustom: stringOrNull(fields.robotsTxtCustom),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function sourceOf(sources, own) {
|
|
77
|
+
const group = sources.find((item) => item.group === SEO_GROUP);
|
|
78
|
+
if (!group) {
|
|
79
|
+
return {
|
|
80
|
+
source: 'channel',
|
|
81
|
+
locked: false,
|
|
82
|
+
inheritable: false,
|
|
83
|
+
overridden: false,
|
|
84
|
+
organizationName: null,
|
|
85
|
+
effective: own,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// The store's own defaults when its own apply: after `set` the sources are the ones read BEFORE
|
|
89
|
+
// the write, so their values would answer the old defaults. The organization's otherwise.
|
|
90
|
+
return {
|
|
91
|
+
source: group.source,
|
|
92
|
+
locked: group.locked,
|
|
93
|
+
inheritable: group.inheritable,
|
|
94
|
+
overridden: group.overridden,
|
|
95
|
+
organizationName: group.organizationName ?? null,
|
|
96
|
+
effective: group.source !== 'channel' && group.values ? defaultsOf(group.values) : own,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function settingsOf(channel, sources) {
|
|
100
|
+
const fields = channel.customFields ?? {};
|
|
101
|
+
const own = defaultsOf(fields);
|
|
102
|
+
return {
|
|
103
|
+
channelId: channel.id,
|
|
104
|
+
defaultLanguageCode: channel.defaultLanguageCode,
|
|
105
|
+
availableLanguageCodes: channel.availableLanguageCodes ?? [channel.defaultLanguageCode],
|
|
106
|
+
storefrontUrl: stringOrNull(fields.storefrontUrl),
|
|
107
|
+
urlLocaleStrategy: stringOrNull(fields.urlLocaleStrategy) ?? 'path-prefix',
|
|
108
|
+
urlLocalePrefixDefault: fields.urlLocalePrefixDefault === true,
|
|
109
|
+
...own,
|
|
110
|
+
defaultsSource: sourceOf(sources, own),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const PATCH_KEYS = [
|
|
114
|
+
'storefrontUrl',
|
|
115
|
+
'urlLocaleStrategy',
|
|
116
|
+
'urlLocalePrefixDefault',
|
|
117
|
+
'twitterHandle',
|
|
118
|
+
'defaultOgImageId',
|
|
119
|
+
'aiBotDefaults',
|
|
120
|
+
'llmsTxtEnabled',
|
|
121
|
+
'robotsTxtCustom',
|
|
122
|
+
];
|
|
123
|
+
const KNOWN_KEYS = new Set(PATCH_KEYS);
|
|
124
|
+
/** What `get` answers beside the settings: sent back in a spread, never written. */
|
|
125
|
+
const READ_ONLY_KEYS = new Set([
|
|
126
|
+
'channelId',
|
|
127
|
+
'defaultLanguageCode',
|
|
128
|
+
'availableLanguageCodes',
|
|
129
|
+
'defaultsSource',
|
|
130
|
+
]);
|
|
131
|
+
function problemsOf(patch) {
|
|
132
|
+
const problems = [];
|
|
133
|
+
const add = (field, message) => problems.push({ field, code: INVALID, message });
|
|
134
|
+
if (!isRecord(patch)) {
|
|
135
|
+
add('patch', "Pass the settings to change as an object, e.g. { urlLocaleStrategy: 'none' }.");
|
|
136
|
+
return problems;
|
|
137
|
+
}
|
|
138
|
+
const named = Object.keys(patch).filter((key) => patch[key] !== undefined && !READ_ONLY_KEYS.has(key));
|
|
139
|
+
if (named.length === 0) {
|
|
140
|
+
add('patch', `Name at least one setting to change: ${PATCH_KEYS.join(', ')}.`);
|
|
141
|
+
}
|
|
142
|
+
for (const key of named) {
|
|
143
|
+
if (!KNOWN_KEYS.has(key)) {
|
|
144
|
+
add(key, `${key} is not an SEO setting; the settings are ${PATCH_KEYS.join(', ')}.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const bots = patch.aiBotDefaults;
|
|
148
|
+
if (bots !== undefined && bots !== null && !isRecord(bots)) {
|
|
149
|
+
// A string would be encoded a second time and stored as a JSON string, not a map.
|
|
150
|
+
add('aiBotDefaults', "aiBotDefaults must be an object of crawler → 'allow' | 'deny' (the SDK encodes it), or null to clear it.");
|
|
151
|
+
}
|
|
152
|
+
return problems;
|
|
153
|
+
}
|
|
154
|
+
/** The custom fields `patch` writes: only the keys it names, the bot map as the column's JSON. */
|
|
155
|
+
function customFieldsOf(patch) {
|
|
156
|
+
const fields = {};
|
|
157
|
+
for (const key of PATCH_KEYS) {
|
|
158
|
+
const value = patch[key];
|
|
159
|
+
if (value === undefined)
|
|
160
|
+
continue;
|
|
161
|
+
fields[key] = key === 'aiBotDefaults' && value !== null ? JSON.stringify(value) : value;
|
|
162
|
+
}
|
|
163
|
+
return fields;
|
|
164
|
+
}
|
|
165
|
+
export function createSeoSettingsModule(transport) {
|
|
166
|
+
const read = async (options) => {
|
|
167
|
+
const data = await transport.graphql({ api: 'admin', query: SEO_SETTINGS_QUERY }, options);
|
|
168
|
+
if (!data.activeChannel) {
|
|
169
|
+
throw new NotFoundError({
|
|
170
|
+
code: 'CHANNEL_NOT_FOUND',
|
|
171
|
+
message: 'No store is active for this session, so it has no SEO settings to read.',
|
|
172
|
+
status: 404,
|
|
173
|
+
service: 'vendure',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return { channel: data.activeChannel, sources: data.channelSettingsSources ?? [] };
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
async get(options) {
|
|
180
|
+
const { channel, sources } = await read(options);
|
|
181
|
+
return settingsOf(channel, sources);
|
|
182
|
+
},
|
|
183
|
+
async set(patch, options) {
|
|
184
|
+
const problems = problemsOf(patch);
|
|
185
|
+
if (problems.length > 0) {
|
|
186
|
+
throw new ValidationError({
|
|
187
|
+
code: INVALID,
|
|
188
|
+
message: problems.map((problem) => problem.message).join(' '),
|
|
189
|
+
status: 400,
|
|
190
|
+
service: 'vendure',
|
|
191
|
+
details: problems,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// The id `updateChannel` needs, read with the SAME call options, so a per-call tenant
|
|
195
|
+
// writes its own store rather than the client's.
|
|
196
|
+
const { channel, sources } = await read(options);
|
|
197
|
+
const data = await transport.graphql({
|
|
198
|
+
api: 'admin',
|
|
199
|
+
query: UPDATE_SEO_SETTINGS_MUTATION,
|
|
200
|
+
variables: { input: { id: channel.id, customFields: customFieldsOf(patch) } },
|
|
201
|
+
}, options);
|
|
202
|
+
if (isErrorResult(data.updateChannel))
|
|
203
|
+
throw errorFromErrorResult(data.updateChannel);
|
|
204
|
+
// An SEO settings write never changes where the defaults come from, so the sources read
|
|
205
|
+
// just before still hold; `sourceOf` takes the new own defaults when the store's apply.
|
|
206
|
+
return settingsOf(data.updateChannel, sources);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
package/dist/shipping/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { type MyparcelModule } from './myparcel.js';
|
|
|
12
12
|
import { type ParcelCountSuggestionMethod } from './parcel-count-suggestion.js';
|
|
13
13
|
import { type ParcelRuleModule } from './parcel-rule.js';
|
|
14
14
|
import { type RateTablesModule } from './rate-tables.js';
|
|
15
|
+
import { type ShipToCountriesModule } from './ship-to-countries.js';
|
|
15
16
|
/** `admin.shipping` (spec §9.2 `shipping`). Always the store the client is scoped to. */
|
|
16
17
|
export interface ShippingModule {
|
|
17
18
|
/** The store's shipping methods: what checkout offers and what each charges. */
|
|
@@ -44,5 +45,10 @@ export interface ShippingModule {
|
|
|
44
45
|
* default profile, and who is emailed per handler when a fulfilment is created.
|
|
45
46
|
*/
|
|
46
47
|
fulfillmentProfiles: FulfillmentProfilesModule;
|
|
48
|
+
/**
|
|
49
|
+
* The countries the store ships to, as checkout applies them: which ones its storefront offers,
|
|
50
|
+
* which selected ones no shopper can choose, saved or previewed.
|
|
51
|
+
*/
|
|
52
|
+
shipToCountries: ShipToCountriesModule;
|
|
47
53
|
}
|
|
48
54
|
export declare function createShippingModule(transport: Transport): ShippingModule;
|
package/dist/shipping/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createMyparcelModule } from './myparcel.js';
|
|
|
11
11
|
import { createParcelCountSuggestion, } from './parcel-count-suggestion.js';
|
|
12
12
|
import { createParcelRuleModule } from './parcel-rule.js';
|
|
13
13
|
import { createRateTablesModule } from './rate-tables.js';
|
|
14
|
+
import { createShipToCountriesModule } from './ship-to-countries.js';
|
|
14
15
|
export function createShippingModule(transport) {
|
|
15
16
|
return {
|
|
16
17
|
methods: createShippingMethodsModule(transport),
|
|
@@ -21,5 +22,6 @@ export function createShippingModule(transport) {
|
|
|
21
22
|
hendriks: createHendriksModule(transport),
|
|
22
23
|
myparcel: createMyparcelModule(transport),
|
|
23
24
|
fulfillmentProfiles: createFulfillmentProfilesModule(transport),
|
|
25
|
+
shipToCountries: createShipToCountriesModule(transport),
|
|
24
26
|
};
|
|
25
27
|
}
|
|
@@ -154,6 +154,11 @@ export interface ShippingMethodsModule {
|
|
|
154
154
|
* - The engine's own input refusal (`ValidationError`, 400): a handler, checker or calculator
|
|
155
155
|
* code it has not registered, a missing argument, or an `extraParcelPrice` outside
|
|
156
156
|
* 0–10,000,000.
|
|
157
|
+
* - `SHIPPING_ZONE_CONFIG_NOT_FOUND` (`NotFoundError`, 404): the `weight-zone-shipping-calculator`
|
|
158
|
+
* names no rate table of the store (`admin.shipping.rateTables`): its `shippingZoneConfigId` is
|
|
159
|
+
* empty or left out (detail code `required`), or is not one of the store's table ids (`enum`;
|
|
160
|
+
* another store's, a deleted one and a malformed one read alike). Such a method would quote no
|
|
161
|
+
* price. `details[0].field` points at the argument (`/input/calculator/arguments/<i>/value`).
|
|
157
162
|
*
|
|
158
163
|
* @effect write
|
|
159
164
|
*/
|
|
@@ -165,7 +170,9 @@ export interface ShippingMethodsModule {
|
|
|
165
170
|
*
|
|
166
171
|
* One method may be held by several stores, and a change reaches every one of them. Refused,
|
|
167
172
|
* with nothing saved, as {@link create} is (`PLUGIN_ACCESS_DENIED` only for a handler, checker
|
|
168
|
-
* or calculator the patch CHANGES: one the method already has is no new pick
|
|
173
|
+
* or calculator the patch CHANGES: one the method already has is no new pick;
|
|
174
|
+
* `SHIPPING_ZONE_CONFIG_NOT_FOUND` whenever the patch sends a weight-zone calculator, judged in
|
|
175
|
+
* the store the client is on, and never when it sends none), and:
|
|
169
176
|
*
|
|
170
177
|
* - `ENTITY_NOT_FOUND` (`NotFoundError`, 404): the store has no such method (another store's
|
|
171
178
|
* reads the same).
|