platform-types 1.10.780 → 1.10.782

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cart.d.ts CHANGED
@@ -538,11 +538,11 @@ declare global {
538
538
  /**
539
539
  * Whether this promotion waives the cart's shipping.
540
540
  *
541
- * ⚠️ `CartTotals.shipping` is structurally `0` today — nothing in the
542
- * api computes a shipping cost yet, by design, because it is chosen at
543
- * checkout. So the waiver currently changes no money, and this flag is the
544
- * only place the GRANT survives to be read by whoever lands the shipping
545
- * quote. Do not read its absence as "shipping was charged".
541
+ * ⚠️ `CartTotals.shipping` stays structurally `0`: the cart has no
542
+ * delivery method, so the shipping cost is chosen at checkout. Storefront
543
+ * checkout reads this flag there: a priced delivery method (`Method.fee`)
544
+ * books its freight line at `0` with `Order.freight.waivedBy: 'promotion'`.
545
+ * Do not read its absence as "shipping was charged".
546
546
  */
547
547
  shippingWaived?: boolean;
548
548
  }
package/dist/cjs/order.js CHANGED
@@ -387,6 +387,15 @@ exports.orderFulfilmentProgress = orderFulfilmentProgress;
387
387
  * - `DELIVERY_METHOD_NOT_FOUND` · 400 — `deliveryMethod` is not in the store's
388
388
  * delivery catalogue, including an absent/empty catalogue. `data: {}`;
389
389
  * refused before reading the cart.
390
+ * - `DELIVERY_FEE_CURRENCY_UNSUPPORTED` · 409 — the chosen delivery method
391
+ * carries a `fee` and the cart is not in the store's order currency
392
+ * (`config.displayCurrency`, pesos when unset), the currency the fee is
393
+ * stated in. `data: {}`; refused after the re-price.
394
+ * - `DELIVERY_FEE_CHANGED` · 409 — `expectedFreight` (absent reads as `0`)
395
+ * differs from the freight the server computes for the chosen delivery
396
+ * method. `data: DeliveryFeeChangedData` carries the server figure; show it
397
+ * and confirm again. Refused after the re-price and the minimum purchase,
398
+ * before any order id, coupon redemption or write.
390
399
  * - `ORDER_INVALID_CART` · 400 — the normalized cart has no finite numeric
391
400
  * `totals.grandTotal`. `data: {}`; refused before repricing/allocation.
392
401
  * - `CART_CHANGED_SINCE_CONFIRM` · 409 — the body sent `expectedCartId` /
@@ -662,6 +671,10 @@ exports.ORDER_ERROR_CODES = [
662
671
  'CART_PRICES_CHANGED',
663
672
  'ORDER_ID_COLLISION',
664
673
  'DELIVERY_METHOD_NOT_FOUND',
674
+ // A priced delivery method (`Method.fee`): the freight moved since the shopper
675
+ // saw it, or the cart is not in the store's order currency.
676
+ 'DELIVERY_FEE_CHANGED',
677
+ 'DELIVERY_FEE_CURRENCY_UNSUPPORTED',
665
678
  'ORDER_BELOW_MINIMUM_PURCHASE',
666
679
  'ORDER_INVALID_CART',
667
680
  // The storefront's confirmed-cart pin (`StorefrontOrderCreateRequest.expectedCartId` / `expectedCartVersion`).
package/dist/cjs/store.js CHANGED
@@ -139,6 +139,14 @@ exports.STORE_MODULE_ERROR_CODES = [
139
139
  * entry naming no list of the store, with `data: { field, value }` where
140
140
  * `field` is `<catalog>[<row>].availability.priceLists[<entry>]`.
141
141
  *
142
+ * - `DELIVERY_FEE_INVALID` — `POST /store` only: a delivery row's `fee` carries
143
+ * an amount or a `freeAbove` that is not a finite number of at least `0` at
144
+ * the order currency's ledger scale, with `data: { field }` where `field` is
145
+ * `deliveryMethods[<row>].fee.<key>`; or a `paymentMethods` row carries a
146
+ * `fee` at all, with `field` `paymentMethods[<row>].fee`. A `fee` that is not
147
+ * an object, or that carries a key other than those two, is
148
+ * `VALIDATION_FAILED`.
149
+ *
142
150
  * Request refusal, `409`:
143
151
  * - `WHOLESALE_LIST_REQUIRED` — an `ecommerce` save turns
144
152
  * `config.wholesaleRequests` on while the store holds no
@@ -177,6 +185,7 @@ exports.STORE_PATCH_ERROR_CODES = [
177
185
  'DISPLAY_CURRENCY_NOT_INVOICEABLE',
178
186
  'PRODUCT_CURRENCY_NOT_INVOICEABLE',
179
187
  'PRICE_LIST_NOT_FOUND',
188
+ 'DELIVERY_FEE_INVALID',
180
189
  'WHOLESALE_LIST_REQUIRED',
181
190
  'FORBIDDEN',
182
191
  'STORE_NOT_FOUND',
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.STOREFRONT_PREVIEW_PROTOCOL_ERRORS = exports.STOREFRONT_PREVIEW_READ_OPERATIONS = exports.STOREFRONT_DESIGN_AUDIENCES = exports.STOREFRONT_BENEFIT_ICONS = exports.STOREFRONT_RESERVED_SLUGS = exports.STOREFRONT_CONTACT_FIELDS = exports.STOREFRONT_PROTECTED_BUILTIN_DESTINATIONS = exports.STOREFRONT_BUILTIN_DESTINATIONS = exports.STOREFRONT_REPEATABLE_SECTION_TYPES = exports.STOREFRONT_SECTION_TYPES = exports.isStorefrontPageSlug = exports.STOREFRONT_PAGE_SLUG_RE = exports.STOREFRONT_DESIGN_LIMITS = exports.STOREFRONT_SOCIAL_HOSTS = void 0;
4
4
  exports.createStorefrontDesignV1 = createStorefrontDesignV1;
5
5
  exports.parseStorefrontPreviewDocument = parseStorefrontPreviewDocument;
6
+ exports.matchesStorefrontVisibility = matchesStorefrontVisibility;
7
+ exports.projectStorefrontDocumentForAudience = projectStorefrontDocumentForAudience;
6
8
  exports.parseStorefrontPreviewMessage = parseStorefrontPreviewMessage;
7
9
  exports.acceptStorefrontPreviewMessage = acceptStorefrontPreviewMessage;
8
10
  /** The social kinds a contact may link, each to its own hosts; a contact links at most one per kind. */
@@ -253,7 +255,7 @@ function previewWithinBytes(value, maximum) {
253
255
  /**
254
256
  * A `visibility` rule kept on the transport only when well-formed: known audiences, and integer
255
257
  * price lists within the store cap. Anything else is left off rather than failing the frame — the
256
- * preview never filters by it, it only shows the rule the editor authored.
258
+ * parser never filters by it, it only carries the rule the editor authored.
257
259
  */
258
260
  function previewVisibility(input) {
259
261
  if (!previewRecord(input) || !Array.isArray(input.audiences))
@@ -362,6 +364,102 @@ function previewAnnouncement(input) {
362
364
  const { visibility, ...rest } = input;
363
365
  return { ...rest, ...previewVisibility(visibility) };
364
366
  }
367
+ function audienceNamed(audiences, audience) {
368
+ return audiences.some((entry) => entry === audience);
369
+ }
370
+ /**
371
+ * The one audience rule: whether `caller` sees an item with this `visibility`. Absent means
372
+ * everyone. Otherwise any listed audience that matches, or the caller's list in `priceLists`:
373
+ * `guest` matches only a guest; `customer` any customer; `retail` / `wholesale` a customer of that
374
+ * class. A rule naming a list deleted since it was authored matches no one through it.
375
+ */
376
+ function matchesStorefrontVisibility(visibility, caller) {
377
+ if (!visibility)
378
+ return true;
379
+ const audiences = Array.isArray(visibility.audiences) ? visibility.audiences : [];
380
+ if (caller.kind === 'guest')
381
+ return audienceNamed(audiences, 'guest');
382
+ if (audienceNamed(audiences, 'customer') || audienceNamed(audiences, caller.audienceClass))
383
+ return true;
384
+ return Array.isArray(visibility.priceLists) && visibility.priceLists.some((id) => id === caller.priceList);
385
+ }
386
+ const isAudienceNode = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
387
+ const audienceNonEmpty = (value) => Array.isArray(value) && value.length > 0;
388
+ function copyAudienceValue(value) {
389
+ if (Array.isArray(value))
390
+ return value.map(copyAudienceValue);
391
+ if (!isAudienceNode(value))
392
+ return value;
393
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, copyAudienceValue(entry)]));
394
+ }
395
+ /**
396
+ * Filters `value` for `caller` into a NEW tree, or `undefined` when it goes. Every `visibility` key
397
+ * is dropped; an item kept because its rule matched gains `audienceScoped: true`. As a published
398
+ * reader drops a link whose target vanished, a link (or a section's action) to a page this caller
399
+ * does not see goes whole. A menu entry left with neither children nor a target, and a footer group
400
+ * left with no links, go too: they would render as dead labels.
401
+ */
402
+ function scopeAudienceValue(value, caller, pages) {
403
+ if (Array.isArray(value))
404
+ return scopeAudienceArray(value, caller, pages);
405
+ if (!isAudienceNode(value))
406
+ return value;
407
+ if (value.kind === 'page')
408
+ return pages.has(String(value.id)) ? copyAudienceValue(value) : undefined;
409
+ const visibility = value.visibility;
410
+ if (!matchesStorefrontVisibility(visibility, caller))
411
+ return undefined;
412
+ const result = Object.fromEntries(scopedAudienceEntries(value, visibility, caller, pages));
413
+ return keepsAfterAudienceFilter(value, result) ? result : undefined;
414
+ }
415
+ /**
416
+ * A kept node's entries, filtered. Built as entries for `Object.fromEntries`, never by assignment:
417
+ * an own `__proto__` key stays a data key and never becomes the result's prototype. A rule-matched
418
+ * item's flag keeps its place, or comes last.
419
+ */
420
+ function scopedAudienceEntries(value, visibility, caller, pages) {
421
+ const entries = [];
422
+ for (const [key, entry] of Object.entries(value)) {
423
+ if (key === 'visibility')
424
+ continue;
425
+ const scoped = key === 'audienceScoped' && visibility ? true : scopeAudienceValue(entry, caller, pages);
426
+ if (scoped !== undefined)
427
+ entries.push([key, scoped]);
428
+ }
429
+ if (visibility && !Object.hasOwn(value, 'audienceScoped'))
430
+ entries.push(['audienceScoped', true]);
431
+ return entries;
432
+ }
433
+ function scopeAudienceArray(items, caller, pages) {
434
+ return items.flatMap((item) => {
435
+ const scoped = scopeAudienceValue(item, caller, pages);
436
+ return scoped === undefined ? [] : [scoped];
437
+ });
438
+ }
439
+ /** Whether a node survives once its own parts are filtered, judged against what it had before. */
440
+ function keepsAfterAudienceFilter(had, result) {
441
+ if (had.target !== undefined && result.target === undefined)
442
+ return false;
443
+ if (audienceNonEmpty(had.children) && !audienceNonEmpty(result.children) && result.target === undefined)
444
+ return false;
445
+ return !(audienceNonEmpty(had.links) && !audienceNonEmpty(result.links));
446
+ }
447
+ /**
448
+ * A storefront design document as `caller` may see it, built as a NEW tree (the input is never
449
+ * changed or shared): drops every section, page, navigation entry (top level or child), footer link
450
+ * and announcement bar `caller` does not match, and every link to a page it does not see; strips
451
+ * every `visibility`; marks an item kept through a rule `audienceScoped: true`. Web `GET /store`
452
+ * applies it to a publication, the preview to its draft for a grant fixture's caller, and a preview
453
+ * frame may apply it to a posted document with the caller its design read echoed.
454
+ */
455
+ function projectStorefrontDocumentForAudience(document, caller) {
456
+ const root = document;
457
+ const pages = new Set((Array.isArray(root.pages) ? root.pages : [])
458
+ .filter(isAudienceNode)
459
+ .filter((page) => matchesStorefrontVisibility(page.visibility, caller))
460
+ .map((page) => String(page.id)));
461
+ return (scopeAudienceValue(root, caller, pages) ?? {});
462
+ }
365
463
  /** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
366
464
  exports.STOREFRONT_PREVIEW_PROTOCOL_ERRORS = [
367
465
  'STOREFRONT_PREVIEW_EXPIRED',
package/dist/order.d.ts CHANGED
@@ -5,6 +5,32 @@ declare global {
5
5
  grandTotal: number;
6
6
  currency: string;
7
7
  }
8
+ /**
9
+ * `409 DELIVERY_FEE_CHANGED`'s `data`: the freight the server computed for
10
+ * the chosen delivery method, in the order currency, `0` when the method is
11
+ * unpriced or its fee is waived. Show it and confirm again with it as
12
+ * `StorefrontOrderCreateRequest.expectedFreight`.
13
+ */
14
+ interface DeliveryFeeChangedData {
15
+ freight: number;
16
+ }
17
+ /**
18
+ * Why a priced delivery method charged nothing: the goods total met the
19
+ * method's `fee.freeAbove`, or the cart's promotion waives shipping.
20
+ */
21
+ type OrderFreightWaiver = 'freeAbove' | 'promotion';
22
+ /**
23
+ * Which priced delivery method booked this order's freight line, and why it
24
+ * charged nothing when it did. METADATA only, never money: the charge is the
25
+ * `lineKind: 'freight'` line in `items`, which is what `total`, the payment
26
+ * attempt, the receivable and the voucher all read.
27
+ */
28
+ interface OrderFreight {
29
+ /** The `Store.deliveryMethods` row id whose `fee` was charged. */
30
+ methodId: number;
31
+ /** Present only when the line booked at 0 because the fee was waived. */
32
+ waivedBy?: OrderFreightWaiver;
33
+ }
8
34
  /**
9
35
  * A coupon as it was redeemed onto an order — the grant frozen at
10
36
  * consumption, plus the money it actually took.
@@ -1189,6 +1215,13 @@ declare global {
1189
1215
  * nothing else can tell a second release from a first.
1190
1216
  */
1191
1217
  coupons?: OrderCoupon[];
1218
+ /**
1219
+ * Present only on a storefront order placed on a delivery method that
1220
+ * carries a `fee` (`Method.fee`), and then always beside exactly one
1221
+ * `lineKind: 'freight'` line. Server-owned: written by storefront checkout
1222
+ * and by no other writer, and stripped from an operator body.
1223
+ */
1224
+ freight?: OrderFreight;
1192
1225
  orderPrinted?: boolean;
1193
1226
  tagPrinted?: boolean;
1194
1227
  /**
@@ -2437,6 +2470,17 @@ declare global {
2437
2470
  * transaction refuses too.
2438
2471
  */
2439
2472
  expectedCartVersion?: number;
2473
+ /**
2474
+ * The freight the storefront showed for `deliveryMethod`, in the order
2475
+ * currency: the row's `fee.amount`, or `0` when `fee.freeAbove` is met by the
2476
+ * cart's `grandTotal` or the cart's promotion waives shipping. Omit it (or
2477
+ * send `0`) for an unpriced method. The server recomputes it and refuses a
2478
+ * different figure `409 DELIVERY_FEE_CHANGED` with `data.freight`
2479
+ * (`DeliveryFeeChangedData`), writing nothing. ⚠️ Omitting it on a method
2480
+ * whose fee comes to more than `0` is refused the same way: the shopper is
2481
+ * never charged a freight they were not shown.
2482
+ */
2483
+ expectedFreight?: number;
2440
2484
  }
2441
2485
  interface ZebraTag {
2442
2486
  orderId: string;
@@ -2818,6 +2862,15 @@ export declare const orderFulfilmentProgress: (order: OrderFulfilmentProgressInp
2818
2862
  * - `DELIVERY_METHOD_NOT_FOUND` · 400 — `deliveryMethod` is not in the store's
2819
2863
  * delivery catalogue, including an absent/empty catalogue. `data: {}`;
2820
2864
  * refused before reading the cart.
2865
+ * - `DELIVERY_FEE_CURRENCY_UNSUPPORTED` · 409 — the chosen delivery method
2866
+ * carries a `fee` and the cart is not in the store's order currency
2867
+ * (`config.displayCurrency`, pesos when unset), the currency the fee is
2868
+ * stated in. `data: {}`; refused after the re-price.
2869
+ * - `DELIVERY_FEE_CHANGED` · 409 — `expectedFreight` (absent reads as `0`)
2870
+ * differs from the freight the server computes for the chosen delivery
2871
+ * method. `data: DeliveryFeeChangedData` carries the server figure; show it
2872
+ * and confirm again. Refused after the re-price and the minimum purchase,
2873
+ * before any order id, coupon redemption or write.
2821
2874
  * - `ORDER_INVALID_CART` · 400 — the normalized cart has no finite numeric
2822
2875
  * `totals.grandTotal`. `data: {}`; refused before repricing/allocation.
2823
2876
  * - `CART_CHANGED_SINCE_CONFIRM` · 409 — the body sent `expectedCartId` /
@@ -2970,6 +3023,6 @@ export declare const orderFulfilmentProgress: (order: OrderFulfilmentProgressInp
2970
3023
  * `SUBSCRIPTION_READONLY`) sit on the order CREATE leg only: an edit is not a new
2971
3024
  * order, so `bareOrderPostHandler` routes past `requireMetered` entirely.
2972
3025
  */
2973
- export declare const ORDER_ERROR_CODES: readonly ["ORDER_COST_DENOMINATION_UNKNOWN", "SERVICE_ORDER_DELIVERY_MANAGED", "ORDER_NOT_FOUND", "ORDER_LOCKED", "ORDER_NOT_READY", "ORDER_DELIVERED_ANOTHER_DAY", "ORDER_DELIVERY_STATE_UNCHANGED", "ORDER_ILLEGAL_TRANSITION", "ORDER_VERSION_CONFLICT", "CUSTOMER_NOT_FOUND", "CUSTOMER_MERGED", "ORDER_HAS_NO_CUSTOMER", "CUSTOMER_ANONYMIZED", "FX_CONVERSION_UNAVAILABLE", "ROLE_FORBIDDEN", "ORDER_CANCELLED", "PAYMENT_SETTLED_ON_ANOTHER_ORDER", "ORDER_HAS_SHIPMENTS", "ORDER_HAS_RETURNS", "SHIPMENT_QUANTITY_EXCEEDED", "SHIPMENT_LINE_MISMATCH", "SHIPMENT_NOT_FOUND", "SHIPMENT_LIMIT_REACHED", "SHIPMENT_TOO_LARGE", "ORDER_FULFILMENT_EXTERNAL", "SHIPMENT_FAILED", "ADMIN_REQUIRED", "IDEMPOTENCY_KEY_REQUIRED", "INVALID_IDEMPOTENCY_KEY", "IDEMPOTENCY_KEY_REUSE", "IDEMPOTENCY_IN_PROGRESS", "IDEMPOTENCY_LOCK_CONTENTION", "IDEMPOTENCY_KEY_FAILED", "IDEMPOTENT_RESPONSE_NOT_CACHED", "ORDER_CANCELLATION_NOT_PROTECTED", "ORDER_CANCELLATION_COMMITTED", "ORDER_CANCELLATION_LOCKED", "ORDER_CANCELLATION_TOO_LARGE", "ORDER_CANCELLATION_FAILED", "SALE_LEDGER_RETRACTION_REFUSED", "SALE_LEDGER_ROW_NOT_OWNED", "PRODUCT_NOT_FOUND", "PRODUCT_SALE_LIMIT_EXCEEDED", "PRODUCT_SALE_LIMIT_INVALID", "DEPOSIT_SOURCE_NOT_ALLOWED", "DEPOSIT_CURRENCY_MISMATCH", "DEPOSIT_EXCEEDS_TOTAL", "DEPOSIT_FREEZE_REQUIRES_INVOICE", "DEPOSIT_FREEZE_INVOICE_PENDING", "DEPOSIT_CHARACTERIZATION_CONFLICT", "DEPOSIT_FREEZE_INVOICE_NOT_QUALIFYING", "DEPOSIT_INVOICED_ORDER_MUST_FREEZE", "DEPOSIT_FROZEN_ORDER_MUST_FREEZE", "DEPOSIT_REFERENCE_IS_LINKED_PAYMENT", "DEPOSIT_DUPLICATE_REFERENCE", "TILL_TENDER_REQUIRES_DEPOSIT", "PAYMENT_REFERENCED_BY_DEPOSIT", "ORDER_HAS_UNREFUNDED_DEPOSIT", "ORDER_PRICE_FROZEN", "DEPOSIT_NOT_FOUND", "DEPOSIT_ALREADY_REFUNDED", "UNIT_QUANTITY_NOT_WHOLE", "UNIT_REVISION_STALE", "UNIT_QUANTITY_UNREPRESENTABLE", "KIT_RECIPE_CHANGED", "CART_PROMOTION_UNAVAILABLE", "ORDER_HAS_KIT_LINES", "ORDER_HAS_FREIGHT_LINE", "ORDER_TARGET_REQUIRED", "ORDER_SOURCE_CONFLICT", "ORDER_CUSTOMER_REQUIRED", "BASKET_EMPTY", "BASKET_CHANGED_DURING_CHECKOUT", "CART_VERSION_CREATE_ONLY", "CART_VERSION_REQUIRES_CART_ID", "CART_REVISION_STALE", "CART_CUSTOMER_MISMATCH", "COUNTER_SALE_CREATE_ONLY", "SOLD_AT_CREATE_ONLY", "SOLD_AT_IN_FUTURE", "ORDER_TENDER_CREATE_ONLY", "CREDIT_OVERRIDE_CREATE_ONLY", "ON_ACCOUNT_CREATE_ONLY", "ON_ACCOUNT_WITH_TENDERS", "TENDERS_REQUIRE_COUNTER_SALE", "LINES_REQUIRE_COUNTER_SALE", "PRODUCT_COST_UNKNOWN", "PRICE_CHANGE_DISABLED", "LINE_DISCOUNT_EXCEEDS_MAX", "CUSTOMER_DISABLED", "SHIFT_NOT_AVAILABLE", "SOLD_AT_OUTSIDE_SHIFT", "ORDER_CURRENCY_NOT_SUBSCRIBED", "PAYMENT_METHODS_NOT_CONFIGURED", "PAYMENT_METHOD_NOT_FOUND", "PAYMENT_METHOD_UNAVAILABLE", "CART_PRICES_CHANGED", "ORDER_ID_COLLISION", "DELIVERY_METHOD_NOT_FOUND", "ORDER_BELOW_MINIMUM_PURCHASE", "ORDER_INVALID_CART", "CART_CHANGED_SINCE_CONFIRM", "EMAIL_VERIFICATION_REQUIRED", "ACCOUNT_DISABLED", "CUSTOMER_ACCESS_PENDING", "CUSTOMER_ACCESS_REJECTED", "CURRENCY_MISMATCH", "SHIFT_NOT_OPEN", "ORDER_DISCOUNT_AFTER_INVOICE", "ORDER_DISCOUNT_ON_SERVICE_ORDER", "ORDER_DISCOUNT_ON_MARKETPLACE_ORDER", "ORDER_INVOICE_METHOD_OUTSTANDING_FACTURA", "ORDER_SERVICE_PERIOD_AFTER_INVOICE", "ORDER_ATTEMPT_COMMITTED", "ORDER_ATTEMPT_ABANDONED", "ORDER_ATTEMPT_KEY_EXPIRED", "ORDER_ATTEMPT_KEY_SCOPE_MISMATCH", "ORDER_COMPLETION_ATTEMPT_COMMITTED", "ORDER_COMPLETION_ATTEMPT_ABANDONED"];
3026
+ export declare const ORDER_ERROR_CODES: readonly ["ORDER_COST_DENOMINATION_UNKNOWN", "SERVICE_ORDER_DELIVERY_MANAGED", "ORDER_NOT_FOUND", "ORDER_LOCKED", "ORDER_NOT_READY", "ORDER_DELIVERED_ANOTHER_DAY", "ORDER_DELIVERY_STATE_UNCHANGED", "ORDER_ILLEGAL_TRANSITION", "ORDER_VERSION_CONFLICT", "CUSTOMER_NOT_FOUND", "CUSTOMER_MERGED", "ORDER_HAS_NO_CUSTOMER", "CUSTOMER_ANONYMIZED", "FX_CONVERSION_UNAVAILABLE", "ROLE_FORBIDDEN", "ORDER_CANCELLED", "PAYMENT_SETTLED_ON_ANOTHER_ORDER", "ORDER_HAS_SHIPMENTS", "ORDER_HAS_RETURNS", "SHIPMENT_QUANTITY_EXCEEDED", "SHIPMENT_LINE_MISMATCH", "SHIPMENT_NOT_FOUND", "SHIPMENT_LIMIT_REACHED", "SHIPMENT_TOO_LARGE", "ORDER_FULFILMENT_EXTERNAL", "SHIPMENT_FAILED", "ADMIN_REQUIRED", "IDEMPOTENCY_KEY_REQUIRED", "INVALID_IDEMPOTENCY_KEY", "IDEMPOTENCY_KEY_REUSE", "IDEMPOTENCY_IN_PROGRESS", "IDEMPOTENCY_LOCK_CONTENTION", "IDEMPOTENCY_KEY_FAILED", "IDEMPOTENT_RESPONSE_NOT_CACHED", "ORDER_CANCELLATION_NOT_PROTECTED", "ORDER_CANCELLATION_COMMITTED", "ORDER_CANCELLATION_LOCKED", "ORDER_CANCELLATION_TOO_LARGE", "ORDER_CANCELLATION_FAILED", "SALE_LEDGER_RETRACTION_REFUSED", "SALE_LEDGER_ROW_NOT_OWNED", "PRODUCT_NOT_FOUND", "PRODUCT_SALE_LIMIT_EXCEEDED", "PRODUCT_SALE_LIMIT_INVALID", "DEPOSIT_SOURCE_NOT_ALLOWED", "DEPOSIT_CURRENCY_MISMATCH", "DEPOSIT_EXCEEDS_TOTAL", "DEPOSIT_FREEZE_REQUIRES_INVOICE", "DEPOSIT_FREEZE_INVOICE_PENDING", "DEPOSIT_CHARACTERIZATION_CONFLICT", "DEPOSIT_FREEZE_INVOICE_NOT_QUALIFYING", "DEPOSIT_INVOICED_ORDER_MUST_FREEZE", "DEPOSIT_FROZEN_ORDER_MUST_FREEZE", "DEPOSIT_REFERENCE_IS_LINKED_PAYMENT", "DEPOSIT_DUPLICATE_REFERENCE", "TILL_TENDER_REQUIRES_DEPOSIT", "PAYMENT_REFERENCED_BY_DEPOSIT", "ORDER_HAS_UNREFUNDED_DEPOSIT", "ORDER_PRICE_FROZEN", "DEPOSIT_NOT_FOUND", "DEPOSIT_ALREADY_REFUNDED", "UNIT_QUANTITY_NOT_WHOLE", "UNIT_REVISION_STALE", "UNIT_QUANTITY_UNREPRESENTABLE", "KIT_RECIPE_CHANGED", "CART_PROMOTION_UNAVAILABLE", "ORDER_HAS_KIT_LINES", "ORDER_HAS_FREIGHT_LINE", "ORDER_TARGET_REQUIRED", "ORDER_SOURCE_CONFLICT", "ORDER_CUSTOMER_REQUIRED", "BASKET_EMPTY", "BASKET_CHANGED_DURING_CHECKOUT", "CART_VERSION_CREATE_ONLY", "CART_VERSION_REQUIRES_CART_ID", "CART_REVISION_STALE", "CART_CUSTOMER_MISMATCH", "COUNTER_SALE_CREATE_ONLY", "SOLD_AT_CREATE_ONLY", "SOLD_AT_IN_FUTURE", "ORDER_TENDER_CREATE_ONLY", "CREDIT_OVERRIDE_CREATE_ONLY", "ON_ACCOUNT_CREATE_ONLY", "ON_ACCOUNT_WITH_TENDERS", "TENDERS_REQUIRE_COUNTER_SALE", "LINES_REQUIRE_COUNTER_SALE", "PRODUCT_COST_UNKNOWN", "PRICE_CHANGE_DISABLED", "LINE_DISCOUNT_EXCEEDS_MAX", "CUSTOMER_DISABLED", "SHIFT_NOT_AVAILABLE", "SOLD_AT_OUTSIDE_SHIFT", "ORDER_CURRENCY_NOT_SUBSCRIBED", "PAYMENT_METHODS_NOT_CONFIGURED", "PAYMENT_METHOD_NOT_FOUND", "PAYMENT_METHOD_UNAVAILABLE", "CART_PRICES_CHANGED", "ORDER_ID_COLLISION", "DELIVERY_METHOD_NOT_FOUND", "DELIVERY_FEE_CHANGED", "DELIVERY_FEE_CURRENCY_UNSUPPORTED", "ORDER_BELOW_MINIMUM_PURCHASE", "ORDER_INVALID_CART", "CART_CHANGED_SINCE_CONFIRM", "EMAIL_VERIFICATION_REQUIRED", "ACCOUNT_DISABLED", "CUSTOMER_ACCESS_PENDING", "CUSTOMER_ACCESS_REJECTED", "CURRENCY_MISMATCH", "SHIFT_NOT_OPEN", "ORDER_DISCOUNT_AFTER_INVOICE", "ORDER_DISCOUNT_ON_SERVICE_ORDER", "ORDER_DISCOUNT_ON_MARKETPLACE_ORDER", "ORDER_INVOICE_METHOD_OUTSTANDING_FACTURA", "ORDER_SERVICE_PERIOD_AFTER_INVOICE", "ORDER_ATTEMPT_COMMITTED", "ORDER_ATTEMPT_ABANDONED", "ORDER_ATTEMPT_KEY_EXPIRED", "ORDER_ATTEMPT_KEY_SCOPE_MISMATCH", "ORDER_COMPLETION_ATTEMPT_COMMITTED", "ORDER_COMPLETION_ATTEMPT_ABANDONED"];
2974
3027
  export type OrderErrorCode = (typeof ORDER_ERROR_CODES)[number];
2975
3028
  export {};
package/dist/order.js CHANGED
@@ -380,6 +380,15 @@ export const orderFulfilmentProgress = (order) => {
380
380
  * - `DELIVERY_METHOD_NOT_FOUND` · 400 — `deliveryMethod` is not in the store's
381
381
  * delivery catalogue, including an absent/empty catalogue. `data: {}`;
382
382
  * refused before reading the cart.
383
+ * - `DELIVERY_FEE_CURRENCY_UNSUPPORTED` · 409 — the chosen delivery method
384
+ * carries a `fee` and the cart is not in the store's order currency
385
+ * (`config.displayCurrency`, pesos when unset), the currency the fee is
386
+ * stated in. `data: {}`; refused after the re-price.
387
+ * - `DELIVERY_FEE_CHANGED` · 409 — `expectedFreight` (absent reads as `0`)
388
+ * differs from the freight the server computes for the chosen delivery
389
+ * method. `data: DeliveryFeeChangedData` carries the server figure; show it
390
+ * and confirm again. Refused after the re-price and the minimum purchase,
391
+ * before any order id, coupon redemption or write.
383
392
  * - `ORDER_INVALID_CART` · 400 — the normalized cart has no finite numeric
384
393
  * `totals.grandTotal`. `data: {}`; refused before repricing/allocation.
385
394
  * - `CART_CHANGED_SINCE_CONFIRM` · 409 — the body sent `expectedCartId` /
@@ -655,6 +664,10 @@ export const ORDER_ERROR_CODES = [
655
664
  'CART_PRICES_CHANGED',
656
665
  'ORDER_ID_COLLISION',
657
666
  'DELIVERY_METHOD_NOT_FOUND',
667
+ // A priced delivery method (`Method.fee`): the freight moved since the shopper
668
+ // saw it, or the cart is not in the store's order currency.
669
+ 'DELIVERY_FEE_CHANGED',
670
+ 'DELIVERY_FEE_CURRENCY_UNSUPPORTED',
658
671
  'ORDER_BELOW_MINIMUM_PURCHASE',
659
672
  'ORDER_INVALID_CART',
660
673
  // The storefront's confirmed-cart pin (`StorefrontOrderCreateRequest.expectedCartId` / `expectedCartVersion`).
package/dist/store.d.ts CHANGED
@@ -1844,6 +1844,45 @@ declare global {
1844
1844
  * is sent back without `availability`; sending a valid rule replaces it.
1845
1845
  */
1846
1846
  availability?: StorefrontDesignVisibility;
1847
+ /**
1848
+ * The price of this DELIVERY method, charged on a storefront order as one
1849
+ * server-owned freight line (`OrderLine.lineKind: 'freight'`). ABSENT means
1850
+ * unpriced: the order books no freight, exactly as before the key existed.
1851
+ * Any delivery row may carry one, the pickup row included. Written by
1852
+ * `POST /store`; carried by every read of a `deliveryMethods` row, the
1853
+ * storefront `GET /store` and the CUSTOMER STORE frame included, because it
1854
+ * is the price the shopper pays.
1855
+ *
1856
+ * DELIVERY rows only. The row type is shared, but `POST /store` refuses a
1857
+ * `fee` on a `paymentMethods` row (`400 DELIVERY_FEE_INVALID`, `data.field`
1858
+ * `paymentMethods[<row>].fee`), and every read drops one a legacy payment
1859
+ * row still holds — nothing charges it there.
1860
+ */
1861
+ fee?: MethodFee;
1862
+ }
1863
+ /**
1864
+ * A delivery method's price, in the store's order currency
1865
+ * (`config.displayCurrency`, pesos when unset) and IVA-inclusive like every
1866
+ * line price.
1867
+ *
1868
+ * `POST /store` refuses `400 DELIVERY_FEE_INVALID` (with `data.field`) unless
1869
+ * both numbers are finite, at least `0`, and representable at that currency's
1870
+ * ledger scale (2 decimals for every seeded currency). On read, a stored fee
1871
+ * is dropped, never coerced, only when it breaks the SHAPE (a key other than
1872
+ * these two, or an amount that is not a finite number of at least `0`); a
1873
+ * stored amount off the ledger scale is still read, and checkout rounds it to
1874
+ * the scale.
1875
+ */
1876
+ interface MethodFee {
1877
+ /** What the method charges. `0` books a freight line at `0`. */
1878
+ amount: number;
1879
+ /**
1880
+ * The goods total from which the method is free: when the cart's
1881
+ * `grandTotal` (after every discount, freight never included) is at least
1882
+ * this, the freight line books at `0` with `Order.freight.waivedBy:
1883
+ * 'freeAbove'`. Absent means never free by amount.
1884
+ */
1885
+ freeAbove?: number;
1847
1886
  }
1848
1887
  interface FiscalCondition {
1849
1888
  CbteTipo: {
@@ -2444,6 +2483,14 @@ export type StoreModuleRefusalBody = {
2444
2483
  * entry naming no list of the store, with `data: { field, value }` where
2445
2484
  * `field` is `<catalog>[<row>].availability.priceLists[<entry>]`.
2446
2485
  *
2486
+ * - `DELIVERY_FEE_INVALID` — `POST /store` only: a delivery row's `fee` carries
2487
+ * an amount or a `freeAbove` that is not a finite number of at least `0` at
2488
+ * the order currency's ledger scale, with `data: { field }` where `field` is
2489
+ * `deliveryMethods[<row>].fee.<key>`; or a `paymentMethods` row carries a
2490
+ * `fee` at all, with `field` `paymentMethods[<row>].fee`. A `fee` that is not
2491
+ * an object, or that carries a key other than those two, is
2492
+ * `VALIDATION_FAILED`.
2493
+ *
2447
2494
  * Request refusal, `409`:
2448
2495
  * - `WHOLESALE_LIST_REQUIRED` — an `ecommerce` save turns
2449
2496
  * `config.wholesaleRequests` on while the store holds no
@@ -2464,5 +2511,5 @@ export type StoreModuleRefusalBody = {
2464
2511
  * checks, the display-currency lock, the storefront fence) are not listed
2465
2512
  * either. Cross-cutting refusals: check `GATING_ERROR_CODES` as well.
2466
2513
  */
2467
- export declare const STORE_PATCH_ERROR_CODES: readonly ["STORE_CONFIG_FORBIDDEN", "STORE_ECOMMERCE_FORBIDDEN", "STORE_FISCAL_FIELD_FORBIDDEN", "STORE_JURISDICTION_FORBIDDEN", "AFIP_RELACION_FLAG_FORBIDDEN", "FX_TABLE_FORBIDDEN", "FX_AUTO_UPDATE_FORBIDDEN", "FIELD_NOT_WRITABLE_HERE", "AFIP_POINT_OF_SALE_NOT_CLEARABLE", "CURRENCY_CATALOG_ID_UNKNOWN", "ARS_RATE_MUST_BE_ONE", "CURRENCY_RATE_ABOVE_CEILING", "FX_CATALOG_ID_UNKNOWN", "FX_SOURCE_ID_UNKNOWN", "DISPLAY_CURRENCY_NOT_INVOICEABLE", "PRODUCT_CURRENCY_NOT_INVOICEABLE", "PRICE_LIST_NOT_FOUND", "WHOLESALE_LIST_REQUIRED", "FORBIDDEN", "STORE_NOT_FOUND"];
2514
+ export declare const STORE_PATCH_ERROR_CODES: readonly ["STORE_CONFIG_FORBIDDEN", "STORE_ECOMMERCE_FORBIDDEN", "STORE_FISCAL_FIELD_FORBIDDEN", "STORE_JURISDICTION_FORBIDDEN", "AFIP_RELACION_FLAG_FORBIDDEN", "FX_TABLE_FORBIDDEN", "FX_AUTO_UPDATE_FORBIDDEN", "FIELD_NOT_WRITABLE_HERE", "AFIP_POINT_OF_SALE_NOT_CLEARABLE", "CURRENCY_CATALOG_ID_UNKNOWN", "ARS_RATE_MUST_BE_ONE", "CURRENCY_RATE_ABOVE_CEILING", "FX_CATALOG_ID_UNKNOWN", "FX_SOURCE_ID_UNKNOWN", "DISPLAY_CURRENCY_NOT_INVOICEABLE", "PRODUCT_CURRENCY_NOT_INVOICEABLE", "PRICE_LIST_NOT_FOUND", "DELIVERY_FEE_INVALID", "WHOLESALE_LIST_REQUIRED", "FORBIDDEN", "STORE_NOT_FOUND"];
2468
2515
  export type StorePatchErrorCode = (typeof STORE_PATCH_ERROR_CODES)[number];
package/dist/store.js CHANGED
@@ -134,6 +134,14 @@ export const STORE_MODULE_ERROR_CODES = [
134
134
  * entry naming no list of the store, with `data: { field, value }` where
135
135
  * `field` is `<catalog>[<row>].availability.priceLists[<entry>]`.
136
136
  *
137
+ * - `DELIVERY_FEE_INVALID` — `POST /store` only: a delivery row's `fee` carries
138
+ * an amount or a `freeAbove` that is not a finite number of at least `0` at
139
+ * the order currency's ledger scale, with `data: { field }` where `field` is
140
+ * `deliveryMethods[<row>].fee.<key>`; or a `paymentMethods` row carries a
141
+ * `fee` at all, with `field` `paymentMethods[<row>].fee`. A `fee` that is not
142
+ * an object, or that carries a key other than those two, is
143
+ * `VALIDATION_FAILED`.
144
+ *
137
145
  * Request refusal, `409`:
138
146
  * - `WHOLESALE_LIST_REQUIRED` — an `ecommerce` save turns
139
147
  * `config.wholesaleRequests` on while the store holds no
@@ -172,6 +180,7 @@ export const STORE_PATCH_ERROR_CODES = [
172
180
  'DISPLAY_CURRENCY_NOT_INVOICEABLE',
173
181
  'PRODUCT_CURRENCY_NOT_INVOICEABLE',
174
182
  'PRICE_LIST_NOT_FOUND',
183
+ 'DELIVERY_FEE_INVALID',
175
184
  'WHOLESALE_LIST_REQUIRED',
176
185
  'FORBIDDEN',
177
186
  'STORE_NOT_FOUND',
@@ -268,12 +268,25 @@ declare global {
268
268
  * so the reference and identity walkers pass over it.
269
269
  *
270
270
  * Web `GET /store` filters by it and strips it, marking each item visible through a rule
271
- * `audienceScoped: true`. The private preview is not filtered and carries it as authored.
271
+ * `audienceScoped: true`. The private preview carries it as authored, unless its grant names a
272
+ * fixture: then it is filtered exactly as web `GET /store` filters it for that fixture's caller.
272
273
  */
273
274
  interface StorefrontDesignVisibility {
274
275
  audiences: StorefrontDesignAudience[];
275
276
  priceLists?: number[];
276
277
  }
278
+ /**
279
+ * Who reads a storefront design, as {@link matchesStorefrontVisibility} judges a `visibility`
280
+ * rule: no approved customer (a guest, a pending or refused registrant included), or one approved
281
+ * customer with its effective list and that list's `audienceClass` (absent meaning `retail`).
282
+ */
283
+ type StorefrontAudienceCaller = {
284
+ kind: 'guest';
285
+ } | {
286
+ kind: 'customer';
287
+ priceList: number;
288
+ audienceClass: 'retail' | 'wholesale';
289
+ };
277
290
  /**
278
291
  * The one announcement bar a design carries above every page, never a second. `text` and
279
292
  * `subtitle` are plain text (no control characters; renderers escape markup); `text` is required
@@ -288,7 +301,7 @@ declare global {
288
301
  *
289
302
  * Every projection drops a disabled bar, as it drops a disabled section. Web `GET /store` also
290
303
  * drops one whose `visibility` the caller does not match; the private preview carries the rule
291
- * as authored.
304
+ * as authored, unless its grant names a fixture, when it drops the bar as web `GET /store` would.
292
305
  */
293
306
  interface StorefrontDesignAnnouncement {
294
307
  id: string;
@@ -414,7 +427,8 @@ declare global {
414
427
  * An item that can carry `visibility` (section, page, navigation entry, footer link, the
415
428
  * announcement bar) may also
416
429
  * carry `audienceScoped: true`: web `GET /store` sets it on an item it returned because the
417
- * caller matched that item's rule, and strips every `visibility`. The preview never sets it.
430
+ * caller matched that item's rule, and strips every `visibility`. The preview sets it only when
431
+ * its grant names a fixture, the same way.
418
432
  */
419
433
  type PublicStorefrontDesignValue<T> = T extends StorefrontDesignImagePlacement ? T & {
420
434
  url: string;
@@ -528,7 +542,27 @@ declare global {
528
542
  type StorefrontDesignDiscardInput = StorefrontDesignRevisionInput;
529
543
  interface StorefrontDesignPreviewInput {
530
544
  draftRevision: string;
545
+ /** Absent: the unfiltered preview at the default list. */
546
+ fixture?: StorefrontPreviewFixture;
531
547
  }
548
+ /**
549
+ * The audience a preview grant renders for — synthetic, never a real customer: no customer row,
550
+ * cart, order or balance is read. `guest` is a visitor without an account. `priceList` is an
551
+ * approved customer on that list of the store (its current `audienceClass` decides `retail` /
552
+ * `wholesale` rules), and every catalogue read prices at it; a list the store does not have is
553
+ * refused on issue, 400 `STOREFRONT_DESIGN_INVALID` with `UNKNOWN_PRICE_LIST` at
554
+ * `fixture.priceList`, and a list deleted after issue reads as a customer on it would (PUBLICO,
555
+ * list 1, with its class). `pending` is a registrant awaiting approval: the api answers it exactly as `guest`
556
+ * (content, methods and prices); the storefront may still show its in-review notices.
557
+ */
558
+ type StorefrontPreviewFixture = {
559
+ kind: 'guest';
560
+ } | {
561
+ kind: 'priceList';
562
+ priceList: number;
563
+ } | {
564
+ kind: 'pending';
565
+ };
532
566
  /**
533
567
  * GET draft answers a baseline built without copying the legacy logo, while POST init copies it,
534
568
  * so `document.branding.logo` can differ between them: clients rebase onto init's document.
@@ -649,11 +683,20 @@ declare global {
649
683
  grantId: string;
650
684
  iat: number;
651
685
  exp: number;
686
+ /** The issue body's fixture, signed in, so only the ADMIN-gated issue route can change it. */
687
+ fixture?: StorefrontPreviewFixture;
652
688
  }
653
689
  type StorefrontPreviewOperation = (typeof STOREFRONT_PREVIEW_READ_OPERATIONS)[number];
654
690
  /** GET query fields use their serialized string representation. Store scope comes only from the grant. */
655
691
  type StorefrontPreviewReadQuery = {
656
- operation: 'design' | 'paymentMethods' | 'deliveryMethods';
692
+ operation: 'design';
693
+ }
694
+ /**
695
+ * The public method rows. With a grant fixture, only the rows that caller may use, as web
696
+ * `GET /store` and checkout judge a row's `availability`; without one, every row.
697
+ */
698
+ | {
699
+ operation: 'paymentMethods' | 'deliveryMethods';
657
700
  }
658
701
  /**
659
702
  * The shop's public web `GET /store` frame — the same allow-listed STORE
@@ -679,7 +722,8 @@ declare global {
679
722
  }
680
723
  /**
681
724
  * `bestSellers` is web `GET /products?mode=best-sellers`'s read and answer, priced as a
682
- * guest. `recentlyBought` answers `{ data: [] }`: a preview has no buyer to read for.
725
+ * guest, or at a `priceList` fixture's list as every catalogue operation is.
726
+ * `recentlyBought` answers `{ data: [] }`: a preview has no buyer to read for.
683
727
  */
684
728
  | {
685
729
  operation: 'bestSellers' | 'recentlyBought';
@@ -769,6 +813,11 @@ declare global {
769
813
  interface StorefrontPreviewDesignResult {
770
814
  mode: 'preview';
771
815
  draftRevision: string;
816
+ /**
817
+ * Without a grant fixture, the draft with every `visibility` as authored. With one, the
818
+ * draft filtered for {@link StorefrontPreviewDesignResult.audience} by
819
+ * {@link projectStorefrontDocumentForAudience}, as web `GET /store` filters a publication.
820
+ */
772
821
  document: PublicStorefrontDesign;
773
822
  literalLayers: {
774
823
  scope: LiteralScope;
@@ -776,12 +825,19 @@ declare global {
776
825
  }[];
777
826
  effectiveSettings: StorefrontDesignSettings;
778
827
  defaultsVersion: string;
828
+ /**
829
+ * The caller the grant's fixture resolved to, present only when the grant names one: filter a
830
+ * posted `StorefrontPreviewDocument` with this same caller and the same walker.
831
+ */
832
+ audience?: StorefrontAudienceCaller;
779
833
  }
780
834
  /**
781
835
  * `operation=store`'s `data`: the fields web `GET /store` builds from the STORE
782
836
  * row, built by the same projector, and `storefrontDesign` from the pinned
783
837
  * draft. The contact is the draft's (`publicContact`, plus `email`/`phone`
784
- * derived from it), as an adopted shop's comes from its publication.
838
+ * derived from it), as an adopted shop's comes from its publication. With a grant
839
+ * fixture, its method rows are only those that caller may use and its design is
840
+ * filtered for that caller, as web `GET /store` answers that caller.
785
841
  *
786
842
  * Never carries what only an open shop's public route knows — `publicStorefront`,
787
843
  * `maintenance` and `globals` — and never answers `STOREFRONT_UNAVAILABLE`: a
@@ -796,7 +852,11 @@ declare global {
796
852
  type: StorefrontDesignSectionType;
797
853
  enabled: boolean;
798
854
  content: Record<string, unknown>;
799
- /** Carried as authored when well-formed, as navigation and footer entries carry theirs; never applied. */
855
+ /**
856
+ * Carried as authored when well-formed, as navigation and footer entries carry theirs; never
857
+ * applied by the parser. A frame whose design read echoed an `audience` should apply it
858
+ * with {@link projectStorefrontDocumentForAudience}.
859
+ */
800
860
  visibility?: StorefrontDesignVisibility;
801
861
  }
802
862
  interface StorefrontPreviewDocument {
@@ -824,7 +884,7 @@ declare global {
824
884
  contact: Record<string, unknown>;
825
885
  seo: object;
826
886
  literalOverrides: Record<string, unknown>;
827
- /** Carried as sent when it is an object, its `visibility` kept only when well-formed; never applied. */
887
+ /** Carried as sent when it is an object, its `visibility` kept only when well-formed; never applied by the parser. */
828
888
  announcement?: object;
829
889
  }
830
890
  type StorefrontPreviewProtocolError = (typeof STOREFRONT_PREVIEW_PROTOCOL_ERRORS)[number];
@@ -931,6 +991,22 @@ declare global {
931
991
  export declare const STOREFRONT_PREVIEW_READ_OPERATIONS: readonly ["design", "store", "product", "productBySlug", "productsByIds", "bestSellers", "recentlyBought", "products", "incomes", "group", "related", "categories", "brands", "tags", "collection", "paymentMethods", "deliveryMethods", "asset"];
932
992
  /** Checks transport structure and UTF-8 size; unfamiliar section types are skipped, never rendered. */
933
993
  export declare function parseStorefrontPreviewDocument(input: unknown): StorefrontPreviewDocument | undefined;
994
+ /**
995
+ * The one audience rule: whether `caller` sees an item with this `visibility`. Absent means
996
+ * everyone. Otherwise any listed audience that matches, or the caller's list in `priceLists`:
997
+ * `guest` matches only a guest; `customer` any customer; `retail` / `wholesale` a customer of that
998
+ * class. A rule naming a list deleted since it was authored matches no one through it.
999
+ */
1000
+ export declare function matchesStorefrontVisibility(visibility: StorefrontDesignVisibility | undefined, caller: StorefrontAudienceCaller): boolean;
1001
+ /**
1002
+ * A storefront design document as `caller` may see it, built as a NEW tree (the input is never
1003
+ * changed or shared): drops every section, page, navigation entry (top level or child), footer link
1004
+ * and announcement bar `caller` does not match, and every link to a page it does not see; strips
1005
+ * every `visibility`; marks an item kept through a rule `audienceScoped: true`. Web `GET /store`
1006
+ * applies it to a publication, the preview to its draft for a grant fixture's caller, and a preview
1007
+ * frame may apply it to a posted document with the caller its design read echoed.
1008
+ */
1009
+ export declare function projectStorefrontDocumentForAudience<T extends object>(document: T, caller: StorefrontAudienceCaller): T;
934
1010
  /** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
935
1011
  export declare const STOREFRONT_PREVIEW_PROTOCOL_ERRORS: readonly ["STOREFRONT_PREVIEW_EXPIRED", "STOREFRONT_PREVIEW_REVOKED", "STOREFRONT_PREVIEW_FORBIDDEN", "STOREFRONT_PREVIEW_UNAVAILABLE", "STOREFRONT_DESIGN_INVALID", "STOREFRONT_SCHEMA_UNSUPPORTED"];
936
1012
  /** Does not authenticate the grant. Keep it in memory and post only to the exact configured target origin. */
@@ -245,7 +245,7 @@ function previewWithinBytes(value, maximum) {
245
245
  /**
246
246
  * A `visibility` rule kept on the transport only when well-formed: known audiences, and integer
247
247
  * price lists within the store cap. Anything else is left off rather than failing the frame — the
248
- * preview never filters by it, it only shows the rule the editor authored.
248
+ * parser never filters by it, it only carries the rule the editor authored.
249
249
  */
250
250
  function previewVisibility(input) {
251
251
  if (!previewRecord(input) || !Array.isArray(input.audiences))
@@ -354,6 +354,102 @@ function previewAnnouncement(input) {
354
354
  const { visibility, ...rest } = input;
355
355
  return { ...rest, ...previewVisibility(visibility) };
356
356
  }
357
+ function audienceNamed(audiences, audience) {
358
+ return audiences.some((entry) => entry === audience);
359
+ }
360
+ /**
361
+ * The one audience rule: whether `caller` sees an item with this `visibility`. Absent means
362
+ * everyone. Otherwise any listed audience that matches, or the caller's list in `priceLists`:
363
+ * `guest` matches only a guest; `customer` any customer; `retail` / `wholesale` a customer of that
364
+ * class. A rule naming a list deleted since it was authored matches no one through it.
365
+ */
366
+ export function matchesStorefrontVisibility(visibility, caller) {
367
+ if (!visibility)
368
+ return true;
369
+ const audiences = Array.isArray(visibility.audiences) ? visibility.audiences : [];
370
+ if (caller.kind === 'guest')
371
+ return audienceNamed(audiences, 'guest');
372
+ if (audienceNamed(audiences, 'customer') || audienceNamed(audiences, caller.audienceClass))
373
+ return true;
374
+ return Array.isArray(visibility.priceLists) && visibility.priceLists.some((id) => id === caller.priceList);
375
+ }
376
+ const isAudienceNode = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
377
+ const audienceNonEmpty = (value) => Array.isArray(value) && value.length > 0;
378
+ function copyAudienceValue(value) {
379
+ if (Array.isArray(value))
380
+ return value.map(copyAudienceValue);
381
+ if (!isAudienceNode(value))
382
+ return value;
383
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, copyAudienceValue(entry)]));
384
+ }
385
+ /**
386
+ * Filters `value` for `caller` into a NEW tree, or `undefined` when it goes. Every `visibility` key
387
+ * is dropped; an item kept because its rule matched gains `audienceScoped: true`. As a published
388
+ * reader drops a link whose target vanished, a link (or a section's action) to a page this caller
389
+ * does not see goes whole. A menu entry left with neither children nor a target, and a footer group
390
+ * left with no links, go too: they would render as dead labels.
391
+ */
392
+ function scopeAudienceValue(value, caller, pages) {
393
+ if (Array.isArray(value))
394
+ return scopeAudienceArray(value, caller, pages);
395
+ if (!isAudienceNode(value))
396
+ return value;
397
+ if (value.kind === 'page')
398
+ return pages.has(String(value.id)) ? copyAudienceValue(value) : undefined;
399
+ const visibility = value.visibility;
400
+ if (!matchesStorefrontVisibility(visibility, caller))
401
+ return undefined;
402
+ const result = Object.fromEntries(scopedAudienceEntries(value, visibility, caller, pages));
403
+ return keepsAfterAudienceFilter(value, result) ? result : undefined;
404
+ }
405
+ /**
406
+ * A kept node's entries, filtered. Built as entries for `Object.fromEntries`, never by assignment:
407
+ * an own `__proto__` key stays a data key and never becomes the result's prototype. A rule-matched
408
+ * item's flag keeps its place, or comes last.
409
+ */
410
+ function scopedAudienceEntries(value, visibility, caller, pages) {
411
+ const entries = [];
412
+ for (const [key, entry] of Object.entries(value)) {
413
+ if (key === 'visibility')
414
+ continue;
415
+ const scoped = key === 'audienceScoped' && visibility ? true : scopeAudienceValue(entry, caller, pages);
416
+ if (scoped !== undefined)
417
+ entries.push([key, scoped]);
418
+ }
419
+ if (visibility && !Object.hasOwn(value, 'audienceScoped'))
420
+ entries.push(['audienceScoped', true]);
421
+ return entries;
422
+ }
423
+ function scopeAudienceArray(items, caller, pages) {
424
+ return items.flatMap((item) => {
425
+ const scoped = scopeAudienceValue(item, caller, pages);
426
+ return scoped === undefined ? [] : [scoped];
427
+ });
428
+ }
429
+ /** Whether a node survives once its own parts are filtered, judged against what it had before. */
430
+ function keepsAfterAudienceFilter(had, result) {
431
+ if (had.target !== undefined && result.target === undefined)
432
+ return false;
433
+ if (audienceNonEmpty(had.children) && !audienceNonEmpty(result.children) && result.target === undefined)
434
+ return false;
435
+ return !(audienceNonEmpty(had.links) && !audienceNonEmpty(result.links));
436
+ }
437
+ /**
438
+ * A storefront design document as `caller` may see it, built as a NEW tree (the input is never
439
+ * changed or shared): drops every section, page, navigation entry (top level or child), footer link
440
+ * and announcement bar `caller` does not match, and every link to a page it does not see; strips
441
+ * every `visibility`; marks an item kept through a rule `audienceScoped: true`. Web `GET /store`
442
+ * applies it to a publication, the preview to its draft for a grant fixture's caller, and a preview
443
+ * frame may apply it to a posted document with the caller its design read echoed.
444
+ */
445
+ export function projectStorefrontDocumentForAudience(document, caller) {
446
+ const root = document;
447
+ const pages = new Set((Array.isArray(root.pages) ? root.pages : [])
448
+ .filter(isAudienceNode)
449
+ .filter((page) => matchesStorefrontVisibility(page.visibility, caller))
450
+ .map((page) => String(page.id)));
451
+ return (scopeAudienceValue(root, caller, pages) ?? {});
452
+ }
357
453
  /** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
358
454
  export const STOREFRONT_PREVIEW_PROTOCOL_ERRORS = [
359
455
  'STOREFRONT_PREVIEW_EXPIRED',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platform-types",
3
- "version": "1.10.780",
3
+ "version": "1.10.782",
4
4
  "description": "Shared wire and entity contracts for the platform.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",