@sixtyninecommerce/admin 0.9.0 → 0.10.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 +294 -0
- package/dist/catalog/products.d.ts +162 -1
- package/dist/catalog/products.js +94 -1
- package/dist/catalog/variants.d.ts +50 -0
- package/dist/catalog/variants.js +48 -18
- package/dist/client.d.ts +21 -0
- package/dist/client.js +6 -0
- package/dist/fulfillments/index.d.ts +77 -0
- package/dist/fulfillments/index.js +109 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +5 -0
- package/dist/integrations/index.d.ts +14 -0
- package/dist/integrations/index.js +9 -0
- package/dist/integrations/pipelines.d.ts +113 -0
- package/dist/integrations/pipelines.js +62 -0
- package/dist/integrations/types.d.ts +176 -0
- package/dist/integrations/types.js +8 -0
- package/dist/jobs/bulk-action-run.d.ts +11 -2
- package/dist/jobs/bulk-action-run.js +4 -5
- package/dist/orders/bulk.d.ts +75 -0
- package/dist/orders/bulk.js +150 -0
- package/dist/orders/deposit-policy.d.ts +103 -0
- package/dist/orders/deposit-policy.js +202 -0
- package/dist/orders/index.d.ts +80 -0
- package/dist/orders/index.js +13 -0
- package/dist/orders/manual-discounts.d.ts +11 -2
- package/dist/orders/manual-discounts.js +2 -0
- package/dist/payments/index.d.ts +7 -0
- package/dist/payments/index.js +2 -0
- package/dist/payments/methods.d.ts +37 -0
- package/dist/payments/methods.js +33 -0
- package/dist/pricing/index.d.ts +3 -0
- package/dist/pricing/index.js +2 -0
- package/dist/pricing/vouchers.d.ts +103 -0
- package/dist/pricing/vouchers.js +69 -0
- package/dist/returns/index.d.ts +225 -0
- package/dist/returns/index.js +160 -0
- package/dist/search/index.d.ts +99 -0
- package/dist/search/index.js +62 -0
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -981,6 +981,32 @@ with the cancel and made right after it commits, so read the order again for the
|
|
|
981
981
|
it cannot be made, the order's history says so and why. A cancel made with no merchant present (an
|
|
982
982
|
integration's own path) refunds automatically.
|
|
983
983
|
|
|
984
|
+
### Handing goods over before an order is paid
|
|
985
|
+
|
|
986
|
+
A store can require full payment before goods leave: its `fulfillmentPaymentPolicy`
|
|
987
|
+
(`{ requireFullPayment, exemptMethodCodes }`, a Channel setting, off by default). Then creating,
|
|
988
|
+
extending or shipping a fulfilment of an order whose COLLECTED money (settled, or held by a payment
|
|
989
|
+
provider — not a bank transfer, an invoice or cash on delivery still to be paid) does not cover what
|
|
990
|
+
it bills is refused `ORDER_NOT_COVERED` (a `BusinessRuleError`), unless the order is on net terms,
|
|
991
|
+
its latest payment is on an exempt method, or an override was recorded. A carrier or 3PL reporting
|
|
992
|
+
goods that already left is recorded, never refused.
|
|
993
|
+
|
|
994
|
+
```ts
|
|
995
|
+
const coverage = await admin.fulfillments.coverage(orderId); // ReadOrder
|
|
996
|
+
// { policyActive: true, covered: false, outstanding: 8100, currencyCode: 'EUR',
|
|
997
|
+
// exempt: false, exemptReason: null, allowed: false, override: null, … }
|
|
998
|
+
const override = await admin.fulfillments.allowUncovered(
|
|
999
|
+
orderId,
|
|
1000
|
+
'Customer pays on pickup', // required, at most 500 characters
|
|
1001
|
+
{ idempotencyKey }
|
|
1002
|
+
); // OverrideFulfillmentPaymentCoverage
|
|
1003
|
+
// { id, outstandingAtGrant: 8100, actorKind: 'user', active: true, … }
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
An override holds while the order owes no more than `outstandingAtGrant` (a refund after the grant
|
|
1007
|
+
ends it); a later grant supersedes an earlier one. Each grant is kept for good and writes a private
|
|
1008
|
+
entry on the order's history.
|
|
1009
|
+
|
|
984
1010
|
### Product feeds
|
|
985
1011
|
|
|
986
1012
|
A feed turns the catalogue into the XML a channel hands Google, Meta, TikTok or Pinterest. The
|
|
@@ -1190,6 +1216,52 @@ const result = await admin.catalog.products.setCollections(productId, selectedId
|
|
|
1190
1216
|
collection with such filters, until its membership job has run). Requires a platform
|
|
1191
1217
|
release that serves `setProductCollections`.
|
|
1192
1218
|
|
|
1219
|
+
#### Saving a product page in one call
|
|
1220
|
+
|
|
1221
|
+
```ts
|
|
1222
|
+
const { product, collections, warnings } = await admin.catalog.products.save(
|
|
1223
|
+
{
|
|
1224
|
+
id: productId,
|
|
1225
|
+
languageCode: 'nl', // names, slugs, descriptions and text values are written in it
|
|
1226
|
+
product: { name: 'Linnen hemd', brand: 'Acme' },
|
|
1227
|
+
variants: [
|
|
1228
|
+
{ id: smallId, price: 5495, prices: [{ currencyCode: 'EUR', price: 5495 }], priceBasis: 'gross' },
|
|
1229
|
+
{ id: mediumId, facetValueIds: [blueId, linenId], gtin: '4006381333931' },
|
|
1230
|
+
],
|
|
1231
|
+
collectionIds: selectedIds,
|
|
1232
|
+
fromCollectionIds: loadedIds,
|
|
1233
|
+
propertyValues: [{ propertyId: originId, value: [portugalItemId] }],
|
|
1234
|
+
},
|
|
1235
|
+
{ idempotencyKey }
|
|
1236
|
+
);
|
|
1237
|
+
for (const warning of warnings) console.warn(warning.code, warning.message);
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
- **All or nothing.** One engine transaction, in this order: the variants, the hand-picked
|
|
1241
|
+
collections (as `setCollections`), the property values (with each filterable product property's
|
|
1242
|
+
label sync), the product. Every step's check runs before the first write, so a refusal of any
|
|
1243
|
+
step leaves nothing written.
|
|
1244
|
+
- **Send the whole form if you like.** The input is a PATCH and the engine writes only what
|
|
1245
|
+
differs from what it holds: a value sent back unchanged is not a write. It never counts as a
|
|
1246
|
+
change of its field group (no "Managed by" badge moves, no `FIELD_GROUP_LOCKED`), and a product
|
|
1247
|
+
the store's organization shares is not refused for a shared field sent back unchanged.
|
|
1248
|
+
- **Prices** state the store's basis (`priceBasis`), checked before sending, as for
|
|
1249
|
+
`variants.update`; `price` and the default-currency entry of `prices` must agree. A currency
|
|
1250
|
+
left out of `prices` keeps its stored price: the list removes nothing.
|
|
1251
|
+
- **Content labels** of content-item values are read from the content system first. When it does
|
|
1252
|
+
not answer, the save still goes through, and `warnings` (`CONTENT_LABELS_NOT_SYNCED`) names the
|
|
1253
|
+
properties whose filter values were saved without their titles; run
|
|
1254
|
+
`catalog.syncContentFacets(propertyId)` on them later.
|
|
1255
|
+
- **Refusals**: `PRODUCT_SAVE_INPUT_INVALID` (400, `error.details` names each field),
|
|
1256
|
+
`PRODUCT_COLLECTIONS_INPUT_INVALID`, `FEED_IDENTIFIER_INVALID`, `ENTITY_NOT_FOUND` (a variant of
|
|
1257
|
+
another product included), `FORBIDDEN` (a step's own permission: `UpdateCatalog` or
|
|
1258
|
+
`UpdateCollection` to change collections, `UpdateCatalog` for property values),
|
|
1259
|
+
`SHARED_*_NOT_ALLOWED`, `MANAGED_BY_ORGANIZATION` (a CHANGED field the organization holds; the
|
|
1260
|
+
store's own price, facet values, collections and property values pass), `FIELD_GROUP_LOCKED`,
|
|
1261
|
+
`PRODUCT_COLLECTION_FILTER_INVALID`, `EXTERNAL_LINE_CARRIER_PROTECTED`.
|
|
1262
|
+
- `idempotencyKey`: a repeat answers the first call's result. Requires a platform release that
|
|
1263
|
+
serves `saveProduct`: deploy the engine before shipping a head that calls it.
|
|
1264
|
+
|
|
1193
1265
|
#### Many products at once
|
|
1194
1266
|
|
|
1195
1267
|
```ts
|
|
@@ -2062,6 +2134,128 @@ An order this store does not hold is a `NotFoundError` (`ENTITY_NOT_FOUND`), the
|
|
|
2062
2134
|
id. This is a root query of its own, never a field on an order list. It needs an engine that serves
|
|
2063
2135
|
`orderStatus`; on an older one only this call fails.
|
|
2064
2136
|
|
|
2137
|
+
### Many orders at once
|
|
2138
|
+
|
|
2139
|
+
```ts
|
|
2140
|
+
const handle = await admin.orders.bulk(
|
|
2141
|
+
{ action: 'MARK_PAID', orderIds: selectedIds }, // TRANSITION (with toState) | CANCEL (reason?) | DELETE | MARK_PAID
|
|
2142
|
+
{ idempotencyKey } // minted when omitted
|
|
2143
|
+
);
|
|
2144
|
+
const run = await handle.wait(); // or later: admin.orders.bulkRun(handle.runId)
|
|
2145
|
+
for (const item of run.items) {
|
|
2146
|
+
if (item.status !== 'FAILED') continue;
|
|
2147
|
+
// ORDER_ACTION_UNAVAILABLE: the order's own status did not offer it; reasonCode says why
|
|
2148
|
+
console.log(item.id, item.code, item.reasonCode, item.message);
|
|
2149
|
+
for (const payment of item.captured ?? []) console.log(payment.paymentId, payment.state, payment.code);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
await admin.orders.delete(orderId); // one draft or cart, as the order page's Delete
|
|
2153
|
+
```
|
|
2154
|
+
|
|
2155
|
+
- **A job, answered at once**, read with `ReadOrder`; `run.items` holds one outcome per order, in
|
|
2156
|
+
the order you named them, and the run's `status` is the run's (a `SUCCEEDED` run can hold
|
|
2157
|
+
`FAILED` orders).
|
|
2158
|
+
- **Each order as on its own page, and on its own.** Its status (`orders.status`) is asked first:
|
|
2159
|
+
an action it does not offer, or a `toState` outside its own `nextStates`, is
|
|
2160
|
+
`ORDER_ACTION_UNAVAILABLE` with `reasonCode` (`STATE`, `INVOICE_LOCKED`, `SHOPPER_CART`,
|
|
2161
|
+
`HAS_RECORDS`, `UNITS_SHIPPED`, …), before anything is written. Then its door, under the door's
|
|
2162
|
+
lock and with its own code (`ORDER_STATE_TRANSITION_ERROR`, `ORDER_CART_NOT_CANCELLABLE`,
|
|
2163
|
+
`ORDER_INVOICE_LOCKED`, `ORDER_HAS_RECORDS`, `ORDER_NOT_DELETED`, …). Another store's order is
|
|
2164
|
+
`ENTITY_NOT_FOUND`.
|
|
2165
|
+
- **`CANCEL` moves no money.** A paid order keeps its payment; refund it with `payments.refund`.
|
|
2166
|
+
- **`MARK_PAID` captures** every payment of the order that is `Authorized` when the run reaches it
|
|
2167
|
+
(read by the engine, never your list), each as `settlePayment` and on its own; `captured` lists
|
|
2168
|
+
them. A decline is the order's result (`SETTLE_PAYMENT_ERROR`), never retried.
|
|
2169
|
+
`IDEMPOTENCY_OUTCOME_UNKNOWN` means the provider was asked and its answer was lost: reconcile
|
|
2170
|
+
with the provider, never retry blind. An order with no authorized payment is
|
|
2171
|
+
`NOTHING_TO_CAPTURE`: it is never marked paid without money. Use `TRANSITION` for any other
|
|
2172
|
+
status; `PaymentSettled`, `Cancelled`, `Modifying` and `AddingItems` are refused there.
|
|
2173
|
+
- **One key, one run, one capture.** The same key with the same input answers the same run;
|
|
2174
|
+
another input under it is `IDEMPOTENCY_CONFLICT`. Each order and each capture is keyed within
|
|
2175
|
+
the run, so a run the platform processes again never asks a provider twice.
|
|
2176
|
+
- **No dry run**, and **at most `MAX_BULK_ORDERS` (50) per run** (`BULK_ACTION_INPUT_INVALID`,
|
|
2177
|
+
refused before sending too). Needs `UpdateOrder` (`DeleteOrder` for `DELETE`).
|
|
2178
|
+
- `delete(orderId)` removes one draft or cart that holds no payment or stock movement; anything
|
|
2179
|
+
else is kept (`ORDER_INVOICE_LOCKED`, `ORDER_HAS_RECORDS`, or `ORDER_NOT_DELETED`, a
|
|
2180
|
+
`ConflictError` like every `*_NOT_DELETED`, with the
|
|
2181
|
+
engine's reason).
|
|
2182
|
+
|
|
2183
|
+
Both need an engine that serves `bulkOrderAction`; on an older one only these calls fail
|
|
2184
|
+
(`GRAPHQL_VALIDATION_FAILED`).
|
|
2185
|
+
|
|
2186
|
+
### Returns
|
|
2187
|
+
|
|
2188
|
+
```ts
|
|
2189
|
+
const rma = await admin.returns.get(returnId); // with its status log; null when absent
|
|
2190
|
+
const returns = await admin.returns.forOrder(orderId); // newest first
|
|
2191
|
+
const eligibility = await admin.returns.eligibility(orderId);
|
|
2192
|
+
|
|
2193
|
+
const plan = await admin.returns.settlementOptions(returnId);
|
|
2194
|
+
// plan.settleable / plan.blockedBy → true / null, or false / 'RECEIPT_REQUIRED'
|
|
2195
|
+
// plan.refund → { available: true, reasonCode: null, payments: [{ paymentId, method, refundable }] }
|
|
2196
|
+
// plan.creditNote → { available: true, ledger: 'RETAIL', reasonCode: null, invoiceNumber: '00042' }
|
|
2197
|
+
// plan.initialMethod → 'refund' | 'store_credit'; plan.defaultAmount → the approved line total
|
|
2198
|
+
```
|
|
2199
|
+
|
|
2200
|
+
The single-return reads need `ReadOrderReturn`. `eligibility` reports the return window and never
|
|
2201
|
+
enforces it: an admin may return outside it, and the quantity caps always apply.
|
|
2202
|
+
|
|
2203
|
+
`settlementOptions` is the plan for settling one return, so a head does not re-derive it. It needs
|
|
2204
|
+
`ReadOrderReturn` and `ReadOrder`, because it names the order's payments. A return this store does
|
|
2205
|
+
not hold is a `NotFoundError`.
|
|
2206
|
+
|
|
2207
|
+
- `settleable` and `blockedBy`: `OUTCOME_UNKNOWN` (the refund it waits on was never confirmed;
|
|
2208
|
+
record the provider's answer first, then it either completes or opens again), `ALREADY_SETTLED`,
|
|
2209
|
+
`IN_PROGRESS` (a refund is on its way), `RECEIPT_REQUIRED` or `STATE`.
|
|
2210
|
+
- `refund`: the settled payments a refund may name, each with what the platform's refund position
|
|
2211
|
+
says it can still give back. That figure holds back a refund whose outcome is unknown and money
|
|
2212
|
+
moved to the customer's account. `NO_REFUNDABLE_PAYMENT` when there is none (net terms, among
|
|
2213
|
+
others).
|
|
2214
|
+
- `creditNote`: `ledger: 'AR'` on a store with an accounts-receivable ledger, where the credit note
|
|
2215
|
+
credits the customer's account; `RETAIL` elsewhere, where it credits the order's invoice and
|
|
2216
|
+
moves no money. Unavailable reasons: `ISSUER_UNAVAILABLE`, `CURRENCY_MISMATCH` (AR),
|
|
2217
|
+
`NO_CREDITABLE_INVOICE` (invoice).
|
|
2218
|
+
- `REFUND_ORPHAN_EXISTS` withholds the refund and an AR credit note while a refund of the invoiced
|
|
2219
|
+
order stands that no credit note names: another payout could give the same money back twice.
|
|
2220
|
+
The invoice credit note stays open.
|
|
2221
|
+
- `initialMethod` preselects the credit note when it was asked for or when no refund is possible,
|
|
2222
|
+
as long as it is available.
|
|
2223
|
+
|
|
2224
|
+
It is a plan: the write (`settleReturn`, not in the SDK yet) loads its own facts and judges again.
|
|
2225
|
+
Against an engine that does not serve `returnSettlementOptions` the call fails validation and
|
|
2226
|
+
nothing else does: deploy vendure-server before relying on it.
|
|
2227
|
+
|
|
2228
|
+
### Deposit policy
|
|
2229
|
+
|
|
2230
|
+
```ts
|
|
2231
|
+
const policy = await admin.orders.depositPolicy.get();
|
|
2232
|
+
// { enabled: false, minimum: 0, allowZeroDepositPark: false, showParkReasonToCustomer: false,
|
|
2233
|
+
// autoCancelAfterDays: null, storefront: { enabled: false, methods: [], minimumPercent: 0 } }
|
|
2234
|
+
|
|
2235
|
+
await admin.orders.depositPolicy.set({ ...policy, enabled: true, minimum: 2_500 }); // €25.00
|
|
2236
|
+
await admin.orders.depositPolicy.set({
|
|
2237
|
+
...policy,
|
|
2238
|
+
enabled: true,
|
|
2239
|
+
storefront: { enabled: true, methods: ['bank-transfer'], minimumPercent: 30 },
|
|
2240
|
+
});
|
|
2241
|
+
```
|
|
2242
|
+
|
|
2243
|
+
Whether the **active** store takes deposits and parks orders (`orders.park`), and whether a shopper
|
|
2244
|
+
may pay a deposit at checkout. Off unless a merchant turns it on: a store that never set one, and a
|
|
2245
|
+
stored value the engine cannot read, answer the defaults above, because `get` reads the policy as
|
|
2246
|
+
the engine applies it. `minimum` is in minor units of the store's DEFAULT currency (an order in
|
|
2247
|
+
another currency is held to it converted at the checkout's rate); `autoCancelAfterDays` is saved
|
|
2248
|
+
but not yet honoured; `storefront` applies only while `enabled` is on too.
|
|
2249
|
+
|
|
2250
|
+
`set` replaces the whole policy: read it, change what you mean to, send it back. Every field is
|
|
2251
|
+
required, only the schema's fields are stored, and a value outside the meta catalog's
|
|
2252
|
+
`order_deposit_policy` bounds is refused before any request with `DEPOSIT_POLICY_INVALID`
|
|
2253
|
+
(`ValidationError`, one `details[]` entry per failing field, such as `storefront.minimumPercent`).
|
|
2254
|
+
The engine does not validate the column, so this check is what keeps a malformed value from
|
|
2255
|
+
silently reading as "deposits off". An absent policy and one set to the defaults mean the same to
|
|
2256
|
+
the engine and the column is never backfilled: write it when a merchant changed it. Writing needs
|
|
2257
|
+
`UpdateChannel`.
|
|
2258
|
+
|
|
2065
2259
|
### Payment providers
|
|
2066
2260
|
|
|
2067
2261
|
Onboard the PSP account a store is paid through, place its card terminals, and ask whether it can
|
|
@@ -2114,6 +2308,23 @@ response carries a link that can be single-use, an idempotency store keeps a res
|
|
|
2114
2308
|
and re-serves it, and retrying is safe anyway — a repeat replaces the pending attempt and answers
|
|
2115
2309
|
with a fresh url and state, which is what a merchant who lost the first link needs.
|
|
2116
2310
|
|
|
2311
|
+
### Payment methods
|
|
2312
|
+
|
|
2313
|
+
```ts
|
|
2314
|
+
const page = await admin.payments.methods.list();
|
|
2315
|
+
page.items; // [{ id, code: 'bank-transfer', name: 'Bank transfer', enabled: true, handlerCode: 'manual-payment' }, …]
|
|
2316
|
+
|
|
2317
|
+
for await (const method of await admin.payments.methods.list()) {
|
|
2318
|
+
if (method.enabled) offer(method.code); // the `method` payments.record takes
|
|
2319
|
+
}
|
|
2320
|
+
```
|
|
2321
|
+
|
|
2322
|
+
The **active** store's payment methods, enabled or not, as staff choose one: `code` is what
|
|
2323
|
+
`payments.record` takes as `method` and what a deposit policy names in `storefront.methods`, and
|
|
2324
|
+
`handlerCode` says what kind of method it is. A page of 100 by default (at most 1,000, or
|
|
2325
|
+
`INVALID_PAGE_SIZE`), walked with `.next()` or `for await`. Needs `ReadSettings` or
|
|
2326
|
+
`ReadPaymentMethod`.
|
|
2327
|
+
|
|
2117
2328
|
### Unapplied cash: an overpayment on the customer's account
|
|
2118
2329
|
|
|
2119
2330
|
A customer who paid more than an order bills (a second transfer, an edit that lowered a paid
|
|
@@ -3239,6 +3450,89 @@ does not hold on the product, names one twice, repeats a SKU, or has a blank or
|
|
|
3239
3450
|
`ORGANIZATION_WORKSPACE_NOT_A_STORE` (422 — called in the workspace) and `ENTITY_NOT_FOUND` (404).
|
|
3240
3451
|
Requires a platform release that serves `makeLocalCopy`.
|
|
3241
3452
|
|
|
3453
|
+
### Data pipeline set-ups (drafts)
|
|
3454
|
+
|
|
3455
|
+
An import or export set-up is kept on the server while the merchant works through it, so it can be
|
|
3456
|
+
continued later, from another device, by any admin of the store with pipeline rights
|
|
3457
|
+
(`UpdateCatalog`). A draft lives **7 days after its last change**; after that it answers
|
|
3458
|
+
`PIPELINE_DRAFT_NOT_FOUND` and the platform deletes it, together with the connection it created and
|
|
3459
|
+
that connection's stored password, unless a pipeline or another draft uses the connection.
|
|
3460
|
+
|
|
3461
|
+
```ts
|
|
3462
|
+
const drafts = admin.integrations.pipelines.drafts;
|
|
3463
|
+
|
|
3464
|
+
let draft = await drafts.create({ direction: 'import' }); // step 'connector', the store's zone
|
|
3465
|
+
draft = await drafts.update(draft.id, { version: draft.version, connectorType: 'woocommerce' });
|
|
3466
|
+
draft = await drafts.update(draft.id, { version: draft.version, sourceUrl: 'https://shop.example.com' });
|
|
3467
|
+
|
|
3468
|
+
// The ONE call that takes credentials: probed, then stored encrypted on the connection.
|
|
3469
|
+
draft = await drafts.connect(draft.id, {
|
|
3470
|
+
version: draft.version,
|
|
3471
|
+
credentials: { consumerKey, consumerSecret },
|
|
3472
|
+
});
|
|
3473
|
+
draft = await drafts.discover(draft.id, { version: draft.version });
|
|
3474
|
+
while (draft.discovery?.status === 'pending' || draft.discovery?.status === 'running') {
|
|
3475
|
+
await sleep(2_000);
|
|
3476
|
+
draft = await drafts.get(draft.id);
|
|
3477
|
+
}
|
|
3478
|
+
draft.dependencyWarnings; // [{ entityType: 'orders', missing: ['shipping_methods'] }]
|
|
3479
|
+
|
|
3480
|
+
draft = await drafts.update(draft.id, { version: draft.version, step: 'configure' }); // gets its name
|
|
3481
|
+
const { pipeline, run, runError } = await drafts.commit(
|
|
3482
|
+
draft.id,
|
|
3483
|
+
{ version: draft.version, startRun: true },
|
|
3484
|
+
{ idempotencyKey: crypto.randomUUID() }
|
|
3485
|
+
);
|
|
3486
|
+
```
|
|
3487
|
+
|
|
3488
|
+
- **A draft never holds a secret.** Credentials go only to `connect`; a `credentials` key anywhere
|
|
3489
|
+
else is refused. A set-up resumed before its connection exists asks for them again.
|
|
3490
|
+
- **Every change names `version`** and answers the next one. A change made on an older version is a
|
|
3491
|
+
`ConflictError` (`PIPELINE_DRAFT_CONFLICT`, 409) that changed nothing: read the draft again.
|
|
3492
|
+
- `connectorType` starts the connection over (even the same connector), `connect` and
|
|
3493
|
+
`useConnection` let go of the previous connection and its discovery, and what a draft lets go of
|
|
3494
|
+
is deleted when a draft created it and nothing else uses it. `useConnection` never deletes a
|
|
3495
|
+
connection that existed before any draft; one another draft created and still holds becomes
|
|
3496
|
+
this draft's too (`connection.createdByDraft`), and the last draft to let go of it unused
|
|
3497
|
+
deletes it.
|
|
3498
|
+
- The default name and the schedule's zone are the **store's** (`storeTimeZone`), DST-safe.
|
|
3499
|
+
- `dependencies` and `importDependencies()` come from the import writers themselves;
|
|
3500
|
+
`dependencyWarnings` names each enabled entity type whose discovered dependencies are switched
|
|
3501
|
+
off. The words are yours.
|
|
3502
|
+
- What a step still needs is a `BusinessRuleError` (`PIPELINE_DRAFT_INCOMPLETE`, 422) with one
|
|
3503
|
+
`details[]` entry per field; a refused change is a `ValidationError` (`PIPELINE_DRAFT_INVALID`).
|
|
3504
|
+
- A commit with `startRun` whose run does not start keeps the pipeline and answers `runError`.
|
|
3505
|
+
- `triggerFilter`, `siteAnalysis`, `publicCredentials` and a schema map's `sourceSchema`,
|
|
3506
|
+
`targetSchema` and `subCounts` are stored data and keep their keys exactly
|
|
3507
|
+
(`PIPELINE_DRAFT_PRESERVE_KEYS`).
|
|
3508
|
+
- Discovery progress: `discovery.status` is read from the draft; the per-phase progress messages
|
|
3509
|
+
are only on the discovery's event stream.
|
|
3510
|
+
|
|
3511
|
+
### Searching the store
|
|
3512
|
+
|
|
3513
|
+
One call finds products, orders, customers and content items matching a term: each type's best
|
|
3514
|
+
matches, **most relevant first**, every one already loaded in the store you act on.
|
|
3515
|
+
|
|
3516
|
+
```ts
|
|
3517
|
+
const found = await admin.search.entities('W4-00087');
|
|
3518
|
+
found.orders[0]; // { id, code, state, orderPlacedAt, totalWithTax, currencyCode, lifecycleStatus }
|
|
3519
|
+
|
|
3520
|
+
const some = await admin.search.entities('serum', { types: ['PRODUCTS', 'CONTENT'], take: 3 });
|
|
3521
|
+
for (const skip of some.skipped) console.log(`${skip.type} not searched: ${skip.reason}`);
|
|
3522
|
+
```
|
|
3523
|
+
|
|
3524
|
+
- **A type you may not read is skipped and named**, never a refusal: its list is empty and
|
|
3525
|
+
`skipped` says why. `PERMISSION` (no `ReadCatalog`/`ReadProduct`, `ReadOrder` or `ReadCustomer`
|
|
3526
|
+
in the store), `CUSTOMER_DATA_NOT_GRANTED` (an app reading customer data below `full` is never
|
|
3527
|
+
searched for orders or customers, which match customers' names and emails), `UNAVAILABLE` (the
|
|
3528
|
+
search service, or the content system, did not answer).
|
|
3529
|
+
- **Content may be missing when the content system is down**: the other types still answer.
|
|
3530
|
+
Content is published items, plus drafts and archived items for a `ReadCmsContent` holder.
|
|
3531
|
+
- A match the store no longer holds is left out, so a list can be shorter than `take`.
|
|
3532
|
+
- `take` is per type, 1 to 10 (`SEARCH_ENTITIES_MAX_TAKE`), 5 when omitted. A term under 2
|
|
3533
|
+
characters (`SEARCH_ENTITIES_MIN_TERM_LENGTH`) or over 500 once trimmed, a `take` out of range or
|
|
3534
|
+
an empty `types` list is refused with `SEARCH_INPUT_INVALID` (`ValidationError`).
|
|
3535
|
+
|
|
3242
3536
|
### Errors
|
|
3243
3537
|
|
|
3244
3538
|
One hierarchy, all re-exported from this package so you need no direct dependency on
|
|
@@ -2,7 +2,8 @@ import { type Page, type Transport } from '@sixtyninecommerce/core';
|
|
|
2
2
|
import type { JobHandle } from '../jobs/index.js';
|
|
3
3
|
import type { Call, ListOptions } from '../types.js';
|
|
4
4
|
import { type ProductBulkInput, type ProductBulkRun } from './products-bulk.js';
|
|
5
|
-
import { type AssetRef, type Availability, type AvailabilityInput, type CatalogueOwnership, type FacetValueRef, type LanguageCode, type ProductAvailabilityState, type TranslationInput } from './types.js';
|
|
5
|
+
import { type AssetRef, type Availability, type AvailabilityInput, type CatalogueOwnership, type FacetValueRef, type LanguageCode, type PriceBasis, type ProductAvailabilityState, type TranslationInput, type VariantAvailabilityState } from './types.js';
|
|
6
|
+
import { type ParcelPacking, type VariantBackorderPolicy, type VariantPriceInput } from './variants.js';
|
|
6
7
|
export interface ProductOptionRef {
|
|
7
8
|
id: string;
|
|
8
9
|
code: string;
|
|
@@ -67,7 +68,15 @@ export interface CreateProductInput extends AvailabilityInput<ProductAvailabilit
|
|
|
67
68
|
assetIds?: string[];
|
|
68
69
|
/** Replaces the product's facet values on update — send the whole set. */
|
|
69
70
|
facetValueIds?: string[];
|
|
71
|
+
/**
|
|
72
|
+
* The brand feeds name; at most 70 characters when new or changed (`FEED_IDENTIFIER_INVALID`
|
|
73
|
+
* otherwise). Null or blank clears it. A stored value sent back unchanged is never judged.
|
|
74
|
+
*/
|
|
75
|
+
brand?: string | null;
|
|
76
|
+
condition?: ProductCondition;
|
|
70
77
|
}
|
|
78
|
+
/** A product's condition, as feeds state it. */
|
|
79
|
+
export type ProductCondition = 'new' | 'refurbished' | 'used';
|
|
71
80
|
/** A PATCH: omitted fields are left alone. */
|
|
72
81
|
export interface UpdateProductInput extends Partial<CreateProductInput> {
|
|
73
82
|
id: string;
|
|
@@ -195,6 +204,109 @@ export interface ProductCollectionsResult {
|
|
|
195
204
|
reason: ProductCollectionNotRemovedReason | (string & {});
|
|
196
205
|
}>;
|
|
197
206
|
}
|
|
207
|
+
/** The product's own fields in a {@link ProductsModule.save}; a PATCH. */
|
|
208
|
+
export interface SaveProductFieldsInput extends AvailabilityInput<ProductAvailabilityState> {
|
|
209
|
+
/** In the save's `languageCode`. */
|
|
210
|
+
name?: string;
|
|
211
|
+
slug?: string;
|
|
212
|
+
description?: string;
|
|
213
|
+
enabled?: boolean;
|
|
214
|
+
/** Null: no featured image. */
|
|
215
|
+
featuredAssetId?: string | null;
|
|
216
|
+
/** The product's images, in display order; replaces the list. */
|
|
217
|
+
assetIds?: string[];
|
|
218
|
+
/** At most 70 characters when new or changed; null or blank clears it. */
|
|
219
|
+
brand?: string | null;
|
|
220
|
+
condition?: ProductCondition;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* One variant in a {@link ProductsModule.save}; a PATCH. A price (`price` or `prices`) travels
|
|
224
|
+
* with `priceBasis`, the store's basis, exactly as for `catalog.variants.update`.
|
|
225
|
+
*/
|
|
226
|
+
export interface SaveProductVariantInput extends AvailabilityInput<VariantAvailabilityState> {
|
|
227
|
+
id: string;
|
|
228
|
+
/** In the save's `languageCode`; trimmed. */
|
|
229
|
+
name?: string;
|
|
230
|
+
sku?: string;
|
|
231
|
+
enabled?: boolean;
|
|
232
|
+
/** Integer minor units of the store's default currency. */
|
|
233
|
+
price?: number;
|
|
234
|
+
/**
|
|
235
|
+
* The price in each named currency. A currency left out keeps its stored price (the list
|
|
236
|
+
* removes nothing), and one equal to what is stored is no change. Its default-currency entry
|
|
237
|
+
* must equal `price` when both are sent.
|
|
238
|
+
*/
|
|
239
|
+
prices?: VariantPriceInput[];
|
|
240
|
+
priceBasis?: PriceBasis;
|
|
241
|
+
taxCategoryId?: string;
|
|
242
|
+
/** Compared as a set. */
|
|
243
|
+
facetValueIds?: string[];
|
|
244
|
+
/** Null: no featured image. */
|
|
245
|
+
featuredAssetId?: string | null;
|
|
246
|
+
assetIds?: string[];
|
|
247
|
+
minOrderQuantity?: number | null;
|
|
248
|
+
salesUnit?: number | null;
|
|
249
|
+
/** Integer minor units of the store's default currency. */
|
|
250
|
+
cost?: number | null;
|
|
251
|
+
packageWeightGrams?: number | null;
|
|
252
|
+
packageLengthCm?: number | null;
|
|
253
|
+
packageWidthCm?: number | null;
|
|
254
|
+
packageHeightCm?: number | null;
|
|
255
|
+
/** 1 to 10,000; null: the store's items per parcel. */
|
|
256
|
+
parcelCapacity?: number | null;
|
|
257
|
+
/** Null: `shared`. */
|
|
258
|
+
parcelPacking?: ParcelPacking | null;
|
|
259
|
+
/** Null: `inherit`. */
|
|
260
|
+
backorderPolicy?: VariantBackorderPolicy | null;
|
|
261
|
+
backorderLimit?: number | null;
|
|
262
|
+
/** 8, 12, 13 or 14 digits when new or changed; null or blank clears it. */
|
|
263
|
+
gtin?: string | null;
|
|
264
|
+
mpn?: string | null;
|
|
265
|
+
}
|
|
266
|
+
/** One property value in a {@link ProductsModule.save}. */
|
|
267
|
+
export interface SaveProductPropertyValueInput {
|
|
268
|
+
propertyId: string;
|
|
269
|
+
/** A translatable property's language; the save's `languageCode` when left out. */
|
|
270
|
+
languageCode?: LanguageCode;
|
|
271
|
+
/** Null, `''` or `[]` clears it; a content-item or product property takes a list of ids. */
|
|
272
|
+
value: unknown;
|
|
273
|
+
}
|
|
274
|
+
/** A product page's whole change, for {@link ProductsModule.save}. */
|
|
275
|
+
export interface SaveProductInput {
|
|
276
|
+
id: string;
|
|
277
|
+
/** The editing language: names, slugs, descriptions and text values are written in it. */
|
|
278
|
+
languageCode: LanguageCode;
|
|
279
|
+
product?: SaveProductFieldsInput;
|
|
280
|
+
variants?: SaveProductVariantInput[];
|
|
281
|
+
/** The collections the product is in by hand: the whole wanted list. */
|
|
282
|
+
collectionIds?: string[];
|
|
283
|
+
/**
|
|
284
|
+
* The collections the caller last read the product in: only the difference is applied, so a
|
|
285
|
+
* change someone else made meanwhile stands (as `setCollections`' `from`). Needs `collectionIds`.
|
|
286
|
+
*/
|
|
287
|
+
fromCollectionIds?: string[];
|
|
288
|
+
/** An absent property keeps its value. */
|
|
289
|
+
propertyValues?: SaveProductPropertyValueInput[];
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Something a save did not do although everything it wrote is saved. A string on the wire: a
|
|
293
|
+
* newer engine may add a code.
|
|
294
|
+
* - `CONTENT_LABELS_NOT_SYNCED`: the content system did not answer, so the filter values of the
|
|
295
|
+
* properties in `propertyIds` were saved without their titles; run
|
|
296
|
+
* `catalog.syncContentFacets(propertyId)` on them once it is back.
|
|
297
|
+
*/
|
|
298
|
+
export interface SaveProductWarning {
|
|
299
|
+
code: 'CONTENT_LABELS_NOT_SYNCED' | (string & {});
|
|
300
|
+
message: string;
|
|
301
|
+
propertyIds: string[];
|
|
302
|
+
}
|
|
303
|
+
export interface SaveProductResult {
|
|
304
|
+
/** The product as saved. */
|
|
305
|
+
product: Product;
|
|
306
|
+
/** What the collection step did; null when the save changed no collection. */
|
|
307
|
+
collections: ProductCollectionsResult | null;
|
|
308
|
+
warnings: SaveProductWarning[];
|
|
309
|
+
}
|
|
198
310
|
/**
|
|
199
311
|
* Product-level catalog writes. Reading needs `ReadCatalog` (or `ReadProduct`),
|
|
200
312
|
* creating `CreateCatalog`, updating `UpdateCatalog`, deleting `DeleteCatalog`.
|
|
@@ -403,6 +515,54 @@ export interface ProductsModule {
|
|
|
403
515
|
setCollections(productId: string, collectionIds: readonly string[], options?: Call & {
|
|
404
516
|
from?: readonly string[];
|
|
405
517
|
}): Promise<ProductCollectionsResult>;
|
|
518
|
+
/**
|
|
519
|
+
* Save a product page's whole change in ONE engine transaction: all of it, or none of it
|
|
520
|
+
* (headless spec §7 `products.save`). In this order: the variants, the hand-picked collections
|
|
521
|
+
* (as {@link setCollections}), the property values (and the filter-label sync of each
|
|
522
|
+
* filterable product property), the product. Every step's check runs before the first write.
|
|
523
|
+
*
|
|
524
|
+
* A PATCH with an echo rule: only what the input names is compared, and only what differs from
|
|
525
|
+
* what the store holds is written. A head may send the whole form: an unchanged value is not a
|
|
526
|
+
* write, so it never records a change of its field group, and a product the store's
|
|
527
|
+
* organization shares is not refused for a shared field sent back unchanged.
|
|
528
|
+
*
|
|
529
|
+
* Labels of content-item values are read from the content system before the transaction. When
|
|
530
|
+
* it does not answer, the save still goes through and `warnings` names the properties whose
|
|
531
|
+
* filter values were saved without their titles.
|
|
532
|
+
*
|
|
533
|
+
* Needs `UpdateCatalog` or `UpdateProduct`, plus `UpdateCatalog` or `UpdateCollection` when it
|
|
534
|
+
* changes collections and `UpdateCatalog` when it changes property values. `idempotencyKey`: a
|
|
535
|
+
* repeat answers the first call's result. Platform releases before this method do not have the
|
|
536
|
+
* mutation and refuse it as `ValidationError` `GRAPHQL_VALIDATION_FAILED` (400): deploy the
|
|
537
|
+
* engine first.
|
|
538
|
+
*
|
|
539
|
+
* Refusals (nothing is written), GraphQL errors mapped by `core`:
|
|
540
|
+
* - `PRICE_BASIS_REQUIRED` / `PRICE_BASIS_MISMATCH` / `INVALID_PRICE` → `ValidationError`
|
|
541
|
+
* (400): a variant price without the store's basis; refused before sending.
|
|
542
|
+
* - `PRODUCT_SAVE_INPUT_INVALID` → `ValidationError` (400): the input names nothing, names a
|
|
543
|
+
* variant, currency or property value twice, uses a language the store does not offer, gives
|
|
544
|
+
* a value outside its field's range, or `price` and the default-currency entry of `prices`
|
|
545
|
+
* differ. `error.details` names each (`field` is a pointer into the call).
|
|
546
|
+
* - `PRODUCT_COLLECTIONS_INPUT_INVALID` → `ValidationError` (400): a collection list names one
|
|
547
|
+
* twice, or more than {@link MAX_PRODUCT_COLLECTION_IDS}; refused before sending too.
|
|
548
|
+
* - `FEED_IDENTIFIER_INVALID` → `ValidationError` (400): a new or changed brand over 70
|
|
549
|
+
* characters, or a GTIN of another shape.
|
|
550
|
+
* - `ENTITY_NOT_FOUND` → `NotFoundError` (404): a product, variant (of another product
|
|
551
|
+
* included), collection or property this store does not hold.
|
|
552
|
+
* - `FORBIDDEN` → `PermissionDeniedError` (403): a step's permission above is missing.
|
|
553
|
+
* - `SHARED_PRODUCT_NOT_ALLOWED`, `SHARED_PRODUCT_VARIANT_NOT_ALLOWED`,
|
|
554
|
+
* `SHARED_COLLECTION_NOT_ALLOWED` → `PermissionDeniedError` (403): a row another store also
|
|
555
|
+
* holds, which you cannot change there.
|
|
556
|
+
* - `MANAGED_BY_ORGANIZATION` → `BusinessRuleError` (422): a CHANGED field of a product, variant,
|
|
557
|
+
* collection or property value the store's organization holds. The store's own price, its own
|
|
558
|
+
* facet values, its own collections and its own property values pass.
|
|
559
|
+
* - `FIELD_GROUP_LOCKED` → `BusinessRuleError` (422): a field group an app or API token owns.
|
|
560
|
+
* - `PRODUCT_COLLECTION_FILTER_INVALID` → `ValidationError` (400): a collection whose
|
|
561
|
+
* hand-picked list cannot be read.
|
|
562
|
+
* - `EXTERNAL_LINE_CARRIER_PROTECTED` → `BusinessRuleError` (422): the platform's carrier.
|
|
563
|
+
* - `INVALID_TAX_CATEGORY_FOR_CLASS` → `ValidationError` (400).
|
|
564
|
+
*/
|
|
565
|
+
save(input: SaveProductInput, options?: Call): Promise<SaveProductResult>;
|
|
406
566
|
/**
|
|
407
567
|
* Enable, disable, delete or clone up to `MAX_BULK_PRODUCTS` (50) products of the store in ONE
|
|
408
568
|
* call (headless spec §7 `products.bulkClone`; §6.11).
|
|
@@ -472,6 +632,7 @@ export declare const SET_ORGANIZATION_PRODUCT_AVAILABILITY_MUTATION = "\n mutat
|
|
|
472
632
|
export declare const ORGANIZATION_PRODUCT_AVAILABILITY_QUERY = "\n query CatalogOrganizationProductAvailability($productIds: [ID!]!, $channelIds: [ID!]!) {\n organizationProductAvailability(productIds: $productIds, channelIds: $channelIds) {\n productId\n channelId\n state\n localCopyId\n }\n }\n";
|
|
473
633
|
export declare const MAKE_LOCAL_COPY_MUTATION = "\n mutation CatalogMakeLocalCopy($productId: ID!, $skus: [LocalCopySkuInput!]) {\n makeLocalCopy(productId: $productId, skus: $skus) {\n product { \n id\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n createdAt\n updatedAt\n languageCode\n name\n slug\n description\n enabled\n featuredAsset { \n id\n name\n preview\n source\n }\n assets { \n id\n name\n preview\n source\n }\n facetValues { \n id\n code\n name\n facet {\n id\n code\n name\n }\n }\n optionGroups {\n id\n code\n name\n options {\n id\n code\n name\n }\n }\n translations {\n id\n languageCode\n name\n slug\n description\n }\n excludedFromStore\n variantList(options: { take: 1 }) {\n totalItems\n }\n customFields {\n availabilityState\n availableFrom\n preorderLimit\n }\n }\n originalProductId\n original\n droppedOrganizationPropertyCount\n }\n }\n";
|
|
474
634
|
export declare const SET_PRODUCT_COLLECTIONS_MUTATION = "\n mutation CatalogSetProductCollections(\n $productId: ID!\n $collectionIds: [ID!]!\n $fromCollectionIds: [ID!]\n ) {\n setProductCollections(\n productId: $productId\n collectionIds: $collectionIds\n fromCollectionIds: $fromCollectionIds\n ) {\n added\n removed\n unchanged\n notRemoved {\n collectionId\n reason\n }\n }\n }\n";
|
|
635
|
+
export declare const SAVE_PRODUCT_MUTATION = "\n mutation CatalogSaveProduct($input: SaveProductInput!, $idempotencyKey: String) {\n saveProduct(input: $input, idempotencyKey: $idempotencyKey) {\n product { \n id\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n createdAt\n updatedAt\n languageCode\n name\n slug\n description\n enabled\n featuredAsset { \n id\n name\n preview\n source\n }\n assets { \n id\n name\n preview\n source\n }\n facetValues { \n id\n code\n name\n facet {\n id\n code\n name\n }\n }\n optionGroups {\n id\n code\n name\n options {\n id\n code\n name\n }\n }\n translations {\n id\n languageCode\n name\n slug\n description\n }\n excludedFromStore\n variantList(options: { take: 1 }) {\n totalItems\n }\n customFields {\n availabilityState\n availableFrom\n preorderLimit\n }\n }\n collections {\n added\n removed\n unchanged\n notRemoved {\n collectionId\n reason\n }\n }\n warnings {\n code\n message\n propertyIds\n }\n }\n }\n";
|
|
475
636
|
/** The product as the Admin API answers it, before `toProduct`. */
|
|
476
637
|
export interface WireProduct extends Omit<Product, keyof Availability<string> | 'variantCount'> {
|
|
477
638
|
variantList: {
|
package/dist/catalog/products.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ValidationError } from '@sixtyninecommerce/core';
|
|
2
2
|
import { bulkProducts, readProductBulkRun, } from './products-bulk.js';
|
|
3
3
|
import { ASSET_REF_FIELDS, assertDeleted, availabilityCustomFields, CATALOGUE_OWNERSHIP_FIELDS, DELETION_FIELDS, FACET_VALUE_REF_FIELDS, fieldOf, unwrapResult, vendureListPage, } from './types.js';
|
|
4
|
+
import { assertPricesStateBasis, } from './variants.js';
|
|
4
5
|
/** The engine's bound on one `exclude` or `include` call (`STORE_EXCLUSION_BATCH_LIMIT`). */
|
|
5
6
|
export const MAX_STORE_EXCLUSION_PRODUCTS = 500;
|
|
6
7
|
/**
|
|
@@ -168,6 +169,27 @@ export const SET_PRODUCT_COLLECTIONS_MUTATION = /* GraphQL */ `
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
`;
|
|
172
|
+
export const SAVE_PRODUCT_MUTATION = /* GraphQL */ `
|
|
173
|
+
mutation CatalogSaveProduct($input: SaveProductInput!, $idempotencyKey: String) {
|
|
174
|
+
saveProduct(input: $input, idempotencyKey: $idempotencyKey) {
|
|
175
|
+
product { ${PRODUCT_FIELDS} }
|
|
176
|
+
collections {
|
|
177
|
+
added
|
|
178
|
+
removed
|
|
179
|
+
unchanged
|
|
180
|
+
notRemoved {
|
|
181
|
+
collectionId
|
|
182
|
+
reason
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
warnings {
|
|
186
|
+
code
|
|
187
|
+
message
|
|
188
|
+
propertyIds
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
171
193
|
/**
|
|
172
194
|
* Lift the availability custom fields to first-class fields. A server older
|
|
173
195
|
* than the pre-order engine has no such fields and would refuse the document
|
|
@@ -184,12 +206,16 @@ export function toProduct(wire) {
|
|
|
184
206
|
};
|
|
185
207
|
}
|
|
186
208
|
function toWireInput(input) {
|
|
187
|
-
const { availabilityState, availableFrom, preorderLimit, ...rest } = input;
|
|
209
|
+
const { availabilityState, availableFrom, preorderLimit, brand, condition, ...rest } = input;
|
|
188
210
|
const customFields = availabilityCustomFields({
|
|
189
211
|
availabilityState,
|
|
190
212
|
availableFrom,
|
|
191
213
|
preorderLimit,
|
|
192
214
|
});
|
|
215
|
+
if (brand !== undefined)
|
|
216
|
+
customFields.brand = brand;
|
|
217
|
+
if (condition !== undefined)
|
|
218
|
+
customFields.condition = condition;
|
|
193
219
|
return Object.keys(customFields).length > 0 ? { ...rest, customFields } : rest;
|
|
194
220
|
}
|
|
195
221
|
export function productFilter(filter) {
|
|
@@ -355,6 +381,15 @@ export function createProductsModule(transport) {
|
|
|
355
381
|
fromCollectionIds: from === undefined ? null : [...from],
|
|
356
382
|
}, call);
|
|
357
383
|
},
|
|
384
|
+
async save(input, options) {
|
|
385
|
+
assertCollectionLists({
|
|
386
|
+
collectionIds: input.collectionIds,
|
|
387
|
+
fromCollectionIds: input.fromCollectionIds,
|
|
388
|
+
});
|
|
389
|
+
await assertPricesStateBasis(transport, input.variants ?? [], options);
|
|
390
|
+
const result = await fieldOf(transport, SAVE_PRODUCT_MUTATION, 'saveProduct', { input: toWireSaveInput(input), idempotencyKey: options?.idempotencyKey ?? null }, options);
|
|
391
|
+
return { ...result, product: toProduct(result.product) };
|
|
392
|
+
},
|
|
358
393
|
bulk(input, options) {
|
|
359
394
|
return bulkProducts(transport, input, options);
|
|
360
395
|
},
|
|
@@ -363,3 +398,61 @@ export function createProductsModule(transport) {
|
|
|
363
398
|
},
|
|
364
399
|
};
|
|
365
400
|
}
|
|
401
|
+
/** The variant fields a save writes as custom fields on the wire. */
|
|
402
|
+
const SAVE_VARIANT_CUSTOM_FIELDS = [
|
|
403
|
+
'minOrderQuantity',
|
|
404
|
+
'salesUnit',
|
|
405
|
+
'cost',
|
|
406
|
+
'packageWeightGrams',
|
|
407
|
+
'packageLengthCm',
|
|
408
|
+
'packageWidthCm',
|
|
409
|
+
'packageHeightCm',
|
|
410
|
+
'parcelCapacity',
|
|
411
|
+
'parcelPacking',
|
|
412
|
+
'backorderPolicy',
|
|
413
|
+
'backorderLimit',
|
|
414
|
+
'gtin',
|
|
415
|
+
'mpn',
|
|
416
|
+
];
|
|
417
|
+
/** The SDK's flat save as the engine's `SaveProductInput`: custom fields nested, no basis. */
|
|
418
|
+
function toWireSaveInput(input) {
|
|
419
|
+
const wire = { id: input.id, languageCode: input.languageCode };
|
|
420
|
+
if (input.product) {
|
|
421
|
+
const { availabilityState, availableFrom, preorderLimit, brand, condition, ...rest } = input.product;
|
|
422
|
+
const customFields = availabilityCustomFields({
|
|
423
|
+
availabilityState,
|
|
424
|
+
availableFrom,
|
|
425
|
+
preorderLimit,
|
|
426
|
+
});
|
|
427
|
+
if (brand !== undefined)
|
|
428
|
+
customFields.brand = brand;
|
|
429
|
+
if (condition !== undefined)
|
|
430
|
+
customFields.condition = condition;
|
|
431
|
+
wire.product = Object.keys(customFields).length > 0 ? { ...rest, customFields } : rest;
|
|
432
|
+
}
|
|
433
|
+
if (input.variants) {
|
|
434
|
+
wire.variants = input.variants.map((variant) => {
|
|
435
|
+
const { availabilityState, availableFrom, preorderLimit, priceBasis, ...rest } = variant;
|
|
436
|
+
void priceBasis;
|
|
437
|
+
const customFields = availabilityCustomFields({
|
|
438
|
+
availabilityState,
|
|
439
|
+
availableFrom,
|
|
440
|
+
preorderLimit,
|
|
441
|
+
});
|
|
442
|
+
const out = { ...rest };
|
|
443
|
+
for (const key of SAVE_VARIANT_CUSTOM_FIELDS) {
|
|
444
|
+
if (key in out) {
|
|
445
|
+
if (out[key] !== undefined)
|
|
446
|
+
customFields[key] = out[key];
|
|
447
|
+
delete out[key];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return Object.keys(customFields).length > 0 ? { ...out, customFields } : out;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
for (const key of ['collectionIds', 'fromCollectionIds', 'propertyValues']) {
|
|
454
|
+
if (input[key] !== undefined)
|
|
455
|
+
wire[key] = input[key];
|
|
456
|
+
}
|
|
457
|
+
return wire;
|
|
458
|
+
}
|