platform-types 1.10.793 → 1.10.795

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/cjs/index.js CHANGED
@@ -55,6 +55,7 @@ __exportStar(require("./favorite.js"), exports);
55
55
  __exportStar(require("./fiscalConditions.js"), exports);
56
56
  __exportStar(require("./gating.js"), exports);
57
57
  __exportStar(require("./guestAttach.js"), exports);
58
+ __exportStar(require("./quoteRequest.js"), exports);
58
59
  __exportStar(require("./guestOrderStatus.js"), exports);
59
60
  __exportStar(require("./holdingCost.js"), exports);
60
61
  __exportStar(require("./demo.js"), exports);
@@ -109,6 +110,7 @@ __exportStar(require("./storefrontTheme.js"), exports);
109
110
  __exportStar(require("./storeNetwork.js"), exports);
110
111
  __exportStar(require("./storefrontAssets.js"), exports);
111
112
  __exportStar(require("./storefrontEvent.js"), exports);
113
+ __exportStar(require("./storefrontIngest.js"), exports);
112
114
  __exportStar(require("./storefrontSeo.js"), exports);
113
115
  __exportStar(require("./storefrontProductMeta.js"), exports);
114
116
  __exportStar(require("./storefrontWebLiteralKeys.js"), exports);
@@ -6,7 +6,8 @@ exports.WEB_NOTIFICATIONS_MAX_LIMIT = exports.CUSTOMER_NOTIFICATION_TYPES = expo
6
6
  // legacy opt-ins and explicit channel maps remain source data on USER rows.
7
7
  // Addressed ERROR / ML_FACTURADOR_COLLISION / ML_SELLER_SWITCHED /
8
8
  // ML_CONNECTION_BROKEN / MENTION / CUSTOMER_PENDING_APPROVAL /
9
- // PAYMENT_ATTEMPT_RESOLUTION / CUSTOMER_WHOLESALE_REQUEST do not select a producer subscriber audience;
9
+ // PAYMENT_ATTEMPT_RESOLUTION / CUSTOMER_WHOLESALE_REQUEST / STOREFRONT_QUOTE_REQUEST do not select a
10
+ // producer subscriber audience;
10
11
  // the consumer still reads the live USER row for each delivery channel.
11
12
  // AFIP_CERT_EXPIRY is seeded on the founding user and uses the subscriber index.
12
13
  // Exported as a real enum consumed by api and app; adding an indexed type
@@ -217,6 +218,24 @@ var NotificationTypeEnum;
217
218
  * audience — the bell would reach the buyer's own storefront socket).
218
219
  */
219
220
  NotificationTypeEnum["CUSTOMER_WHOLESALE_REQUEST"] = "CUSTOMER_WHOLESALE_REQUEST";
221
+ /**
222
+ * A shopper sent a quote request from a storefront `quoteRequest` section
223
+ * (web `POST /quote-requests`) and it waits in the operator inbox
224
+ * (`GET /quote-requests`). The bell names nobody — the request's contact
225
+ * data is PII and NOTIF# rows outlive an erasure.
226
+ *
227
+ * ⚠️ ADMIN-ONLY, and its own preference key. The producer fans out to every
228
+ * non-disabled ADMIN of the store (`listActiveAdmins`), the same recipients
229
+ * as `CUSTOMER_PENDING_APPROVAL`. No legacy opt-in scan: the consumer
230
+ * enforces explicit type/channel preferences
231
+ * (`notificationPreferences.STOREFRONT_QUOTE_REQUEST`), default-on when
232
+ * absent. Additive — no backfill or consumer migration owed.
233
+ *
234
+ * ⚠️ Carries `quoteRequestRef` and NEVER a top-level `customerId` (an
235
+ * audience — the bell would reach a signed-in requester's own storefront
236
+ * socket).
237
+ */
238
+ NotificationTypeEnum["STOREFRONT_QUOTE_REQUEST"] = "STOREFRONT_QUOTE_REQUEST";
220
239
  })(NotificationTypeEnum || (exports.NotificationTypeEnum = NotificationTypeEnum = {}));
221
240
  /**
222
241
  * The notification types a CUSTOMER bell can carry today; each links through
@@ -132,6 +132,11 @@ exports.PRESUPUESTO_IMMUTABLE_STATUSES = ['rejected', 'expired'];
132
132
  * ⚠️ `GET` has no such gate.
133
133
  * - `INVALID_CURSOR` · 400 — a malformed `?startKey=`, or one minted against a
134
134
  * different store or index. Read path only.
135
+ * - `QUOTE_REQUEST_NOT_FOUND` · 404 — a create naming a `quoteRequestId` this
136
+ * store has no request for. Nothing is written and no number is spent.
137
+ * - `QUOTE_REQUEST_ALREADY_LINKED` · 409 — a create naming a quote request that a
138
+ * presupuesto already converted, found by the read before the write or by the
139
+ * write's own condition when two creates race. No presupuesto is written.
135
140
  * - `PRODUCT_COST_UNKNOWN` · 400 — a conversion naming a product whose catalogue
136
141
  * row states no `cost`, refused rather than booking an order with a COGS
137
142
  * nobody can state. The same body the POS checkout renders.
@@ -176,4 +181,7 @@ exports.PRESUPUESTO_ERROR_CODES = [
176
181
  // The per-sale cap refusal every sale entry point shares (`ProductSaleLimitRefusal`).
177
182
  'PRODUCT_SALE_LIMIT_EXCEEDED',
178
183
  'PRODUCT_SALE_LIMIT_INVALID',
184
+ // The create's storefront quote-request link (`quoteRequestId`).
185
+ 'QUOTE_REQUEST_NOT_FOUND',
186
+ 'QUOTE_REQUEST_ALREADY_LINKED',
179
187
  ];
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Storefront quote requests — a shopper (guest or signed-in) asks the shop for a
4
+ * quote from a `quoteRequest` storefront section; the operator reads it in the
5
+ * back office and turns it into a Presupuesto.
6
+ *
7
+ * Web (public, bound origin only): `POST /quote-requests`.
8
+ * App (ADMIN, SUPERVISOR, MANAGER): `GET /quote-requests` (list and read) and
9
+ * `POST /quote-requests { mode: 'status' }`. A Presupuesto created with
10
+ * `quoteRequestId` links the request (`POST /presupuestos`).
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.QUOTE_REQUEST_RECAPTCHA_ACTION = exports.QUOTE_REQUEST_LIMITS = exports.QUOTE_REQUEST_OPTIONAL_FIELDS = exports.QUOTE_REQUEST_STATUSES = exports.QUOTE_REQUEST_ERROR_CODES = void 0;
14
+ /**
15
+ * The refusals of the quote-request routes.
16
+ *
17
+ * - INVALID_QUOTE_REQUEST (400): a body over 8 KiB, malformed or off-schema; a guest
18
+ * without `contact`; `items` when no placed `quoteRequest` section allows them
19
+ * (`fieldErrors` names `items`); a guest `contact.phone` / `company` / `cuit` no
20
+ * placed section lists in `fields` (names `contact.<field>` — refused, never
21
+ * dropped); or an `items[i]` naming a product that does not exist or is not
22
+ * offered on the storefront (names `items.<i>.productId`).
23
+ * - CAPTCHA_INVALID (403): the `x-captcha-token` header is missing, invalid or
24
+ * scored too low. One body whatever the reason.
25
+ * - ORIGIN_STORE_MISMATCH (403): the request's Origin is not bound to a shop, or
26
+ * is bound to another shop than the one it names. Nothing is stored.
27
+ * - QUOTE_REQUESTS_DISABLED (403): the shop's live publication has no enabled
28
+ * `quoteRequest` section on an enabled page — it never placed the form, or
29
+ * unpublished it. Nothing is stored.
30
+ * - QUOTE_REQUEST_LIMIT (429): the shop's daily cap for guests, or the hourly
31
+ * cool-down of the guest's address or the signed-in buyer's account, is spent.
32
+ * Nothing is stored.
33
+ * - QUOTE_REQUEST_NOT_FOUND (404): app routes, an unknown `quoteRequestId`.
34
+ * - QUOTE_REQUEST_ALREADY_LINKED (409): `POST /presupuestos` naming a request
35
+ * another Presupuesto already converted. No Presupuesto is written. Also the
36
+ * app `POST /quote-requests { mode: 'status' }` on a converted request: its
37
+ * status belongs to the Presupuesto that converted it and no longer changes.
38
+ */
39
+ exports.QUOTE_REQUEST_ERROR_CODES = [
40
+ 'INVALID_QUOTE_REQUEST',
41
+ 'CAPTCHA_INVALID',
42
+ 'ORIGIN_STORE_MISMATCH',
43
+ 'QUOTE_REQUESTS_DISABLED',
44
+ 'QUOTE_REQUEST_LIMIT',
45
+ 'QUOTE_REQUEST_NOT_FOUND',
46
+ 'QUOTE_REQUEST_ALREADY_LINKED',
47
+ ];
48
+ exports.QUOTE_REQUEST_STATUSES = ['new', 'in-progress', 'converted', 'dismissed'];
49
+ /** The optional contact fields a `quoteRequest` section may ask for. */
50
+ exports.QUOTE_REQUEST_OPTIONAL_FIELDS = ['phone', 'company', 'cuit'];
51
+ /** Bounds of the web write. */
52
+ exports.QUOTE_REQUEST_LIMITS = {
53
+ message: 2000,
54
+ items: 50,
55
+ /** The shared base-quantity ceiling (`baseQuantitySchema`), six decimal places. */
56
+ itemQuantity: 1_000_000,
57
+ name: 120,
58
+ company: 120,
59
+ email: 254,
60
+ phone: 40,
61
+ };
62
+ /** The reCAPTCHA action the storefront executes for the web write. */
63
+ exports.QUOTE_REQUEST_RECAPTCHA_ACTION = 'quote_request';
@@ -93,6 +93,8 @@ exports.STOREFRONT_SECTION_TYPES = [
93
93
  'paymentDelivery',
94
94
  'logoWall',
95
95
  'gallery',
96
+ 'quoteRequest',
97
+ 'customerRequest',
96
98
  ];
97
99
  /**
98
100
  * The section types a page may repeat, each up to `STOREFRONT_DESIGN_LIMITS.repeatableSectionsPerType`
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /**
3
+ * Guest storefront telemetry ingest — the HTTP route a shopper who never signs
4
+ * in sends first-party storefront events through (the signed-in shopper keeps
5
+ * the WebSocket `logs` action).
6
+ *
7
+ * Web (public, bound origin only): `POST /events` (a batch) and
8
+ * `POST /events/consent` (an analytics-consent withdrawal for one journey).
9
+ *
10
+ * The tenant is the request's bound Origin (a slug host or a verified custom
11
+ * domain), never `x-store` and never an event's `tenant_store_id`. Nothing a
12
+ * guest sends can claim to be a customer: `customer_id` is not part of a guest
13
+ * event, and one that carries it is dropped and counted.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.STOREFRONT_INGEST_LIMITS = exports.STOREFRONT_INGEST_ERROR_CODES = void 0;
17
+ /**
18
+ * The refusals of the guest ingest routes.
19
+ *
20
+ * - EVENTS_TENANT_UNRESOLVED (403): the request's Origin is bound to no shop.
21
+ * Checked before the body is read; nothing is stored.
22
+ * - EVENTS_BATCH_TOO_LARGE (413): the raw body is over
23
+ * {@link STOREFRONT_INGEST_LIMITS.maxBodyBytes} bytes, or `events` holds more
24
+ * than {@link STOREFRONT_INGEST_LIMITS.maxEvents} entries. Nothing is stored.
25
+ * - VALIDATION_FAILED (400): the request as a WHOLE is malformed — not JSON, not
26
+ * the envelope shape, or (withdrawal) `anonymousId` not a UUID. A single event
27
+ * that does not validate is never a 400: it is a counted drop in `dropped`.
28
+ * - EVENTS_RATE_LIMITED (429): the source's budget is spent. Nothing is stored;
29
+ * retry later.
30
+ */
31
+ exports.STOREFRONT_INGEST_ERROR_CODES = [
32
+ 'EVENTS_TENANT_UNRESOLVED',
33
+ 'EVENTS_BATCH_TOO_LARGE',
34
+ 'VALIDATION_FAILED',
35
+ 'EVENTS_RATE_LIMITED',
36
+ ];
37
+ /** Bounds of `POST /events`, enforced before any event is read. */
38
+ exports.STOREFRONT_INGEST_LIMITS = {
39
+ /** The raw request body ceiling, in UTF-8 bytes, measured before parsing. */
40
+ maxBodyBytes: 32 * 1024,
41
+ /** At most this many events per request. */
42
+ maxEvents: 25,
43
+ /** The longest `analyticsConsent.version` accepted; a longer one never stores an event. */
44
+ consentVersionMaxLength: 64,
45
+ };
@@ -1273,6 +1273,17 @@ declare global {
1273
1273
  * the request flow).
1274
1274
  */
1275
1275
  wholesaleRequests?: WholesaleRequestExportEntry[];
1276
+ /**
1277
+ * The subject's storefront quote requests, newest first, on BOTH surfaces:
1278
+ * the ones they sent signed in under their own id plus any filed under a
1279
+ * merged-away id folded into them (the same folded ids, and the same cap, as
1280
+ * the financial collections). Each is the request as the operator reads it —
1281
+ * the contact copied from their account, the message, the items and its
1282
+ * status. A request sent as a guest carries no customer id and is not
1283
+ * reachable from here. Absent when there is none (and from servers predating
1284
+ * storefront quote requests).
1285
+ */
1286
+ quoteRequests?: QuoteRequest[];
1276
1287
  /**
1277
1288
  * Per COLLECTION, and honest: `true` means the read stopped before the
1278
1289
  * end of a partition, folded financial IDs were omitted at the cap, or whole
@@ -1302,6 +1313,12 @@ declare global {
1302
1313
  * may be missing.
1303
1314
  */
1304
1315
  wholesaleRequests?: boolean;
1316
+ /**
1317
+ * Present when `quoteRequests` is, or when merged-away ids were cut at the
1318
+ * folded-id cap — then `true`: a request filed under an omitted id, or
1319
+ * past the per-export request cap, may be missing.
1320
+ */
1321
+ quoteRequests?: boolean;
1305
1322
  };
1306
1323
  }
1307
1324
  /**
@@ -1609,6 +1626,24 @@ declare global {
1609
1626
  * (`WebAnonymizeCustomerResult`).
1610
1627
  */
1611
1628
  rmaEraseComplete: boolean;
1629
+ /**
1630
+ * Storefront quote requests the subject sent signed in
1631
+ * (`QUOTEREQUEST#{storeId}`), DELETED by this erasure — the whole row goes:
1632
+ * it holds their contact and free text. Found through the per-customer
1633
+ * pointer row, which goes last. A Presupuesto raised from one keeps its
1634
+ * `sourceQuoteRequestId`, now dangling. Optional only so the field is
1635
+ * additive — every erasure from this version on sets it.
1636
+ */
1637
+ quoteRequestsErased?: number;
1638
+ /**
1639
+ * `false` when the subject's quote requests could not be read or deleted
1640
+ * to the end, so one may remain. Re-issuing the erasure re-runs the step;
1641
+ * BOTH erasure handlers write a deduped `ERROR` row
1642
+ * (`CUSTOMER_ANONYMIZE_QUOTE_REQUESTS_INCOMPLETE`). On BOTH wires — the
1643
+ * subject's own data. Optional only so the field is additive; only a
1644
+ * measured `true` is a finished step.
1645
+ */
1646
+ quoteRequestEraseComplete?: boolean;
1612
1647
  /**
1613
1648
  * The subject's `ORDER#` rows whose frozen recipient this erasure scrubbed —
1614
1649
  * `customer` reduced to `{ customerId }`, `deliveryAddress` removed, the
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ export * from './favorite.ts';
31
31
  export * from './fiscalConditions.ts';
32
32
  export * from './gating.ts';
33
33
  export * from './guestAttach.ts';
34
+ export * from './quoteRequest.ts';
34
35
  export * from './guestOrderStatus.ts';
35
36
  export * from './holdingCost.ts';
36
37
  export * from './demo.ts';
@@ -85,6 +86,7 @@ export * from './storefrontTheme.ts';
85
86
  export * from './storeNetwork.ts';
86
87
  export * from './storefrontAssets.ts';
87
88
  export * from './storefrontEvent.ts';
89
+ export * from './storefrontIngest.ts';
88
90
  export * from './storefrontSeo.ts';
89
91
  export * from './storefrontProductMeta.ts';
90
92
  export * from './storefrontWebLiteralKeys.ts';
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ export * from "./favorite.js";
39
39
  export * from "./fiscalConditions.js";
40
40
  export * from "./gating.js";
41
41
  export * from "./guestAttach.js";
42
+ export * from "./quoteRequest.js";
42
43
  export * from "./guestOrderStatus.js";
43
44
  export * from "./holdingCost.js";
44
45
  export * from "./demo.js";
@@ -93,6 +94,7 @@ export * from "./storefrontTheme.js";
93
94
  export * from "./storeNetwork.js";
94
95
  export * from "./storefrontAssets.js";
95
96
  export * from "./storefrontEvent.js";
97
+ export * from "./storefrontIngest.js";
96
98
  export * from "./storefrontSeo.js";
97
99
  export * from "./storefrontProductMeta.js";
98
100
  export * from "./storefrontWebLiteralKeys.js";
@@ -195,7 +195,25 @@ export declare enum NotificationTypeEnum {
195
195
  * ⚠️ Carries `wholesaleRequestRef` and NEVER a top-level `customerId` (an
196
196
  * audience — the bell would reach the buyer's own storefront socket).
197
197
  */
198
- CUSTOMER_WHOLESALE_REQUEST = "CUSTOMER_WHOLESALE_REQUEST"
198
+ CUSTOMER_WHOLESALE_REQUEST = "CUSTOMER_WHOLESALE_REQUEST",
199
+ /**
200
+ * A shopper sent a quote request from a storefront `quoteRequest` section
201
+ * (web `POST /quote-requests`) and it waits in the operator inbox
202
+ * (`GET /quote-requests`). The bell names nobody — the request's contact
203
+ * data is PII and NOTIF# rows outlive an erasure.
204
+ *
205
+ * ⚠️ ADMIN-ONLY, and its own preference key. The producer fans out to every
206
+ * non-disabled ADMIN of the store (`listActiveAdmins`), the same recipients
207
+ * as `CUSTOMER_PENDING_APPROVAL`. No legacy opt-in scan: the consumer
208
+ * enforces explicit type/channel preferences
209
+ * (`notificationPreferences.STOREFRONT_QUOTE_REQUEST`), default-on when
210
+ * absent. Additive — no backfill or consumer migration owed.
211
+ *
212
+ * ⚠️ Carries `quoteRequestRef` and NEVER a top-level `customerId` (an
213
+ * audience — the bell would reach a signed-in requester's own storefront
214
+ * socket).
215
+ */
216
+ STOREFRONT_QUOTE_REQUEST = "STOREFRONT_QUOTE_REQUEST"
199
217
  }
200
218
  /**
201
219
  * The notification types a CUSTOMER bell can carry today; each links through
@@ -313,6 +331,11 @@ declare global {
313
331
  * an audience — see {@link WholesaleRequestRef}.
314
332
  */
315
333
  wholesaleRequestRef?: WholesaleRequestRef;
334
+ /**
335
+ * `STOREFRONT_QUOTE_REQUEST` only: which request to open. A POINTER, not
336
+ * an audience — see {@link QuoteRequestRef}. Operator surfaces only.
337
+ */
338
+ quoteRequestRef?: QuoteRequestRef;
316
339
  /** `MENTION` only: the colleague who wrote the mention (a snapshot). */
317
340
  actor?: NotificationActor;
318
341
  userId?: string;
@@ -376,7 +399,7 @@ declare global {
376
399
  * customer socket carry — the stored row minus the operator-only pointers.
377
400
  * A union keyed on `type`; `ORDER` is its only member today.
378
401
  */
379
- type CustomerNotification = Omit<NotificationInterface, 'cartRef' | 'pendingCustomerRef' | 'wholesaleRequestRef' | 'TableName' | 'type' | 'orderId'> & {
402
+ type CustomerNotification = Omit<NotificationInterface, 'cartRef' | 'pendingCustomerRef' | 'wholesaleRequestRef' | 'quoteRequestRef' | 'TableName' | 'type' | 'orderId'> & {
380
403
  type: NotificationTypeEnum.ORDER;
381
404
  /** Set on every customer row: the order the bell is about. */
382
405
  orderId: string;
@@ -3,7 +3,8 @@
3
3
  // legacy opt-ins and explicit channel maps remain source data on USER rows.
4
4
  // Addressed ERROR / ML_FACTURADOR_COLLISION / ML_SELLER_SWITCHED /
5
5
  // ML_CONNECTION_BROKEN / MENTION / CUSTOMER_PENDING_APPROVAL /
6
- // PAYMENT_ATTEMPT_RESOLUTION / CUSTOMER_WHOLESALE_REQUEST do not select a producer subscriber audience;
6
+ // PAYMENT_ATTEMPT_RESOLUTION / CUSTOMER_WHOLESALE_REQUEST / STOREFRONT_QUOTE_REQUEST do not select a
7
+ // producer subscriber audience;
7
8
  // the consumer still reads the live USER row for each delivery channel.
8
9
  // AFIP_CERT_EXPIRY is seeded on the founding user and uses the subscriber index.
9
10
  // Exported as a real enum consumed by api and app; adding an indexed type
@@ -214,6 +215,24 @@ export var NotificationTypeEnum;
214
215
  * audience — the bell would reach the buyer's own storefront socket).
215
216
  */
216
217
  NotificationTypeEnum["CUSTOMER_WHOLESALE_REQUEST"] = "CUSTOMER_WHOLESALE_REQUEST";
218
+ /**
219
+ * A shopper sent a quote request from a storefront `quoteRequest` section
220
+ * (web `POST /quote-requests`) and it waits in the operator inbox
221
+ * (`GET /quote-requests`). The bell names nobody — the request's contact
222
+ * data is PII and NOTIF# rows outlive an erasure.
223
+ *
224
+ * ⚠️ ADMIN-ONLY, and its own preference key. The producer fans out to every
225
+ * non-disabled ADMIN of the store (`listActiveAdmins`), the same recipients
226
+ * as `CUSTOMER_PENDING_APPROVAL`. No legacy opt-in scan: the consumer
227
+ * enforces explicit type/channel preferences
228
+ * (`notificationPreferences.STOREFRONT_QUOTE_REQUEST`), default-on when
229
+ * absent. Additive — no backfill or consumer migration owed.
230
+ *
231
+ * ⚠️ Carries `quoteRequestRef` and NEVER a top-level `customerId` (an
232
+ * audience — the bell would reach a signed-in requester's own storefront
233
+ * socket).
234
+ */
235
+ NotificationTypeEnum["STOREFRONT_QUOTE_REQUEST"] = "STOREFRONT_QUOTE_REQUEST";
217
236
  })(NotificationTypeEnum || (NotificationTypeEnum = {}));
218
237
  /**
219
238
  * The notification types a CUSTOMER bell can carry today; each links through
@@ -135,6 +135,18 @@ declare global {
135
135
  * write rather than minting a duplicate order.
136
136
  */
137
137
  convertedOrderId?: string;
138
+ /**
139
+ * The storefront quote request (`QuoteRequest.quoteRequestId`) this
140
+ * presupuesto was raised from — `POST /presupuestos` with `quoteRequestId`.
141
+ * Written in the SAME transaction that stamps `linkedPresupuestoId` and
142
+ * `status: 'converted'` on the request, so the two halves of the link
143
+ * cannot disagree. Absent on a presupuesto raised from scratch.
144
+ *
145
+ * ⚠️ The request can disappear later — a customer erasure deletes their
146
+ * requests and a guest request expires — so a reader must treat a dangling
147
+ * id as "the request is gone", never as an error.
148
+ */
149
+ sourceQuoteRequestId?: string;
138
150
  createdAt: number;
139
151
  updatedAt?: number;
140
152
  /** Lowercase '#'-joined write-side index. Internal; not part of the read contract. */
@@ -226,6 +238,11 @@ export declare const PRESUPUESTO_IMMUTABLE_STATUSES: readonly ["rejected", "expi
226
238
  * ⚠️ `GET` has no such gate.
227
239
  * - `INVALID_CURSOR` · 400 — a malformed `?startKey=`, or one minted against a
228
240
  * different store or index. Read path only.
241
+ * - `QUOTE_REQUEST_NOT_FOUND` · 404 — a create naming a `quoteRequestId` this
242
+ * store has no request for. Nothing is written and no number is spent.
243
+ * - `QUOTE_REQUEST_ALREADY_LINKED` · 409 — a create naming a quote request that a
244
+ * presupuesto already converted, found by the read before the write or by the
245
+ * write's own condition when two creates race. No presupuesto is written.
229
246
  * - `PRODUCT_COST_UNKNOWN` · 400 — a conversion naming a product whose catalogue
230
247
  * row states no `cost`, refused rather than booking an order with a COGS
231
248
  * nobody can state. The same body the POS checkout renders.
@@ -246,6 +263,6 @@ export declare const PRESUPUESTO_IMMUTABLE_STATUSES: readonly ["rejected", "expi
246
263
  * operator can act on is the distinct-product ceiling those items derive
247
264
  * from, and the two refuse at exactly the same point.
248
265
  */
249
- export declare const PRESUPUESTO_ERROR_CODES: readonly ["PRESUPUESTO_NOT_FOUND", "VALIDATION_FAILED", "ADMIN_REQUIRED", "INVALID_CURSOR", "PRODUCT_COST_UNKNOWN", "PRESUPUESTO_NOT_MUTABLE", "PRESUPUESTO_ILLEGAL_TRANSITION", "PRESUPUESTO_ALREADY_CONVERTED", "PRESUPUESTO_NOT_APPROVED", "PRESUPUESTO_TOO_MANY_PRODUCTS", "TRANSACTION_ITEMS_LIMIT_REACHED", "PRODUCT_NOT_FOUND", "UNIT_QUANTITY_NOT_WHOLE", "UNIT_QUANTITY_UNREPRESENTABLE", "UNIT_REVISION_STALE", "STOCK INSUFICIENTE", "PRODUCT_SALE_LIMIT_EXCEEDED", "PRODUCT_SALE_LIMIT_INVALID"];
266
+ export declare const PRESUPUESTO_ERROR_CODES: readonly ["PRESUPUESTO_NOT_FOUND", "VALIDATION_FAILED", "ADMIN_REQUIRED", "INVALID_CURSOR", "PRODUCT_COST_UNKNOWN", "PRESUPUESTO_NOT_MUTABLE", "PRESUPUESTO_ILLEGAL_TRANSITION", "PRESUPUESTO_ALREADY_CONVERTED", "PRESUPUESTO_NOT_APPROVED", "PRESUPUESTO_TOO_MANY_PRODUCTS", "TRANSACTION_ITEMS_LIMIT_REACHED", "PRODUCT_NOT_FOUND", "UNIT_QUANTITY_NOT_WHOLE", "UNIT_QUANTITY_UNREPRESENTABLE", "UNIT_REVISION_STALE", "STOCK INSUFICIENTE", "PRODUCT_SALE_LIMIT_EXCEEDED", "PRODUCT_SALE_LIMIT_INVALID", "QUOTE_REQUEST_NOT_FOUND", "QUOTE_REQUEST_ALREADY_LINKED"];
250
267
  export type PresupuestoErrorCode = (typeof PRESUPUESTO_ERROR_CODES)[number];
251
268
  export {};
@@ -129,6 +129,11 @@ export const PRESUPUESTO_IMMUTABLE_STATUSES = ['rejected', 'expired'];
129
129
  * ⚠️ `GET` has no such gate.
130
130
  * - `INVALID_CURSOR` · 400 — a malformed `?startKey=`, or one minted against a
131
131
  * different store or index. Read path only.
132
+ * - `QUOTE_REQUEST_NOT_FOUND` · 404 — a create naming a `quoteRequestId` this
133
+ * store has no request for. Nothing is written and no number is spent.
134
+ * - `QUOTE_REQUEST_ALREADY_LINKED` · 409 — a create naming a quote request that a
135
+ * presupuesto already converted, found by the read before the write or by the
136
+ * write's own condition when two creates race. No presupuesto is written.
132
137
  * - `PRODUCT_COST_UNKNOWN` · 400 — a conversion naming a product whose catalogue
133
138
  * row states no `cost`, refused rather than booking an order with a COGS
134
139
  * nobody can state. The same body the POS checkout renders.
@@ -173,4 +178,7 @@ export const PRESUPUESTO_ERROR_CODES = [
173
178
  // The per-sale cap refusal every sale entry point shares (`ProductSaleLimitRefusal`).
174
179
  'PRODUCT_SALE_LIMIT_EXCEEDED',
175
180
  'PRODUCT_SALE_LIMIT_INVALID',
181
+ // The create's storefront quote-request link (`quoteRequestId`).
182
+ 'QUOTE_REQUEST_NOT_FOUND',
183
+ 'QUOTE_REQUEST_ALREADY_LINKED',
176
184
  ];
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Storefront quote requests — a shopper (guest or signed-in) asks the shop for a
3
+ * quote from a `quoteRequest` storefront section; the operator reads it in the
4
+ * back office and turns it into a Presupuesto.
5
+ *
6
+ * Web (public, bound origin only): `POST /quote-requests`.
7
+ * App (ADMIN, SUPERVISOR, MANAGER): `GET /quote-requests` (list and read) and
8
+ * `POST /quote-requests { mode: 'status' }`. A Presupuesto created with
9
+ * `quoteRequestId` links the request (`POST /presupuestos`).
10
+ */
11
+ /**
12
+ * The refusals of the quote-request routes.
13
+ *
14
+ * - INVALID_QUOTE_REQUEST (400): a body over 8 KiB, malformed or off-schema; a guest
15
+ * without `contact`; `items` when no placed `quoteRequest` section allows them
16
+ * (`fieldErrors` names `items`); a guest `contact.phone` / `company` / `cuit` no
17
+ * placed section lists in `fields` (names `contact.<field>` — refused, never
18
+ * dropped); or an `items[i]` naming a product that does not exist or is not
19
+ * offered on the storefront (names `items.<i>.productId`).
20
+ * - CAPTCHA_INVALID (403): the `x-captcha-token` header is missing, invalid or
21
+ * scored too low. One body whatever the reason.
22
+ * - ORIGIN_STORE_MISMATCH (403): the request's Origin is not bound to a shop, or
23
+ * is bound to another shop than the one it names. Nothing is stored.
24
+ * - QUOTE_REQUESTS_DISABLED (403): the shop's live publication has no enabled
25
+ * `quoteRequest` section on an enabled page — it never placed the form, or
26
+ * unpublished it. Nothing is stored.
27
+ * - QUOTE_REQUEST_LIMIT (429): the shop's daily cap for guests, or the hourly
28
+ * cool-down of the guest's address or the signed-in buyer's account, is spent.
29
+ * Nothing is stored.
30
+ * - QUOTE_REQUEST_NOT_FOUND (404): app routes, an unknown `quoteRequestId`.
31
+ * - QUOTE_REQUEST_ALREADY_LINKED (409): `POST /presupuestos` naming a request
32
+ * another Presupuesto already converted. No Presupuesto is written. Also the
33
+ * app `POST /quote-requests { mode: 'status' }` on a converted request: its
34
+ * status belongs to the Presupuesto that converted it and no longer changes.
35
+ */
36
+ export declare const QUOTE_REQUEST_ERROR_CODES: readonly ["INVALID_QUOTE_REQUEST", "CAPTCHA_INVALID", "ORIGIN_STORE_MISMATCH", "QUOTE_REQUESTS_DISABLED", "QUOTE_REQUEST_LIMIT", "QUOTE_REQUEST_NOT_FOUND", "QUOTE_REQUEST_ALREADY_LINKED"];
37
+ export declare const QUOTE_REQUEST_STATUSES: readonly ["new", "in-progress", "converted", "dismissed"];
38
+ /** The optional contact fields a `quoteRequest` section may ask for. */
39
+ export declare const QUOTE_REQUEST_OPTIONAL_FIELDS: readonly ["phone", "company", "cuit"];
40
+ /** Bounds of the web write. */
41
+ export declare const QUOTE_REQUEST_LIMITS: {
42
+ readonly message: 2000;
43
+ readonly items: 50;
44
+ /** The shared base-quantity ceiling (`baseQuantitySchema`), six decimal places. */
45
+ readonly itemQuantity: 1000000;
46
+ readonly name: 120;
47
+ readonly company: 120;
48
+ readonly email: 254;
49
+ readonly phone: 40;
50
+ };
51
+ /** The reCAPTCHA action the storefront executes for the web write. */
52
+ export declare const QUOTE_REQUEST_RECAPTCHA_ACTION = "quote_request";
53
+ declare global {
54
+ type QuoteRequestErrorCode = (typeof QUOTE_REQUEST_ERROR_CODES)[number];
55
+ type QuoteRequestStatus = (typeof QUOTE_REQUEST_STATUSES)[number];
56
+ type QuoteRequestOptionalField = (typeof QUOTE_REQUEST_OPTIONAL_FIELDS)[number];
57
+ interface QuoteRequestContact {
58
+ name: string;
59
+ email: string;
60
+ phone?: string;
61
+ company?: string;
62
+ /** Digits only, 11 when present. */
63
+ cuit?: string;
64
+ }
65
+ interface QuoteRequestItem {
66
+ productId: string;
67
+ quantity: number;
68
+ /** The product's name when the request was sent — never a price. */
69
+ name: string;
70
+ }
71
+ /**
72
+ * Web `POST /quote-requests` body. The tenant is the request's bound Origin,
73
+ * never `x-store`; the reCAPTCHA token rides the `x-captcha-token` header.
74
+ *
75
+ * - `contact` is REQUIRED for a guest and IGNORED for a signed-in buyer, whose
76
+ * contact is copied from their account. A guest's `phone`, `company` and `cuit`
77
+ * are accepted only when a placed section lists them in `fields`.
78
+ * - `items` is accepted only when a placed section sets `allowItems`.
79
+ * - `website` is a honeypot the storefront renders hidden and leaves empty: when
80
+ * filled, the request answers 201 with a `quoteRequestId` and stores nothing.
81
+ */
82
+ interface QuoteRequestCreateInput {
83
+ contact?: QuoteRequestContact;
84
+ message: string;
85
+ items?: {
86
+ productId: string;
87
+ quantity: number;
88
+ }[];
89
+ website?: string;
90
+ }
91
+ /** Web `POST /quote-requests` 201 `data`. */
92
+ interface QuoteRequestCreateResult {
93
+ quoteRequestId: string;
94
+ }
95
+ /** The stored request, as the app routes answer it. */
96
+ interface QuoteRequest {
97
+ quoteRequestId: string;
98
+ status: QuoteRequestStatus;
99
+ /** Set only for a signed-in buyer, from the bearer. */
100
+ customerId?: string;
101
+ contact: QuoteRequestContact;
102
+ message: string;
103
+ items?: QuoteRequestItem[];
104
+ /** The Presupuesto that converted this request. */
105
+ linkedPresupuestoId?: string;
106
+ createdAt: number;
107
+ updatedAt: number;
108
+ }
109
+ /** App `POST /quote-requests { mode: 'status' }` body. `converted` is set only by a Presupuesto create. */
110
+ interface QuoteRequestStatusInput {
111
+ mode: 'status';
112
+ quoteRequestId: string;
113
+ status: Exclude<QuoteRequestStatus, 'converted'>;
114
+ }
115
+ /** What a `STOREFRONT_QUOTE_REQUEST` operator bell points at. */
116
+ interface QuoteRequestRef {
117
+ quoteRequestId: string;
118
+ }
119
+ }
120
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Storefront quote requests — a shopper (guest or signed-in) asks the shop for a
3
+ * quote from a `quoteRequest` storefront section; the operator reads it in the
4
+ * back office and turns it into a Presupuesto.
5
+ *
6
+ * Web (public, bound origin only): `POST /quote-requests`.
7
+ * App (ADMIN, SUPERVISOR, MANAGER): `GET /quote-requests` (list and read) and
8
+ * `POST /quote-requests { mode: 'status' }`. A Presupuesto created with
9
+ * `quoteRequestId` links the request (`POST /presupuestos`).
10
+ */
11
+ /**
12
+ * The refusals of the quote-request routes.
13
+ *
14
+ * - INVALID_QUOTE_REQUEST (400): a body over 8 KiB, malformed or off-schema; a guest
15
+ * without `contact`; `items` when no placed `quoteRequest` section allows them
16
+ * (`fieldErrors` names `items`); a guest `contact.phone` / `company` / `cuit` no
17
+ * placed section lists in `fields` (names `contact.<field>` — refused, never
18
+ * dropped); or an `items[i]` naming a product that does not exist or is not
19
+ * offered on the storefront (names `items.<i>.productId`).
20
+ * - CAPTCHA_INVALID (403): the `x-captcha-token` header is missing, invalid or
21
+ * scored too low. One body whatever the reason.
22
+ * - ORIGIN_STORE_MISMATCH (403): the request's Origin is not bound to a shop, or
23
+ * is bound to another shop than the one it names. Nothing is stored.
24
+ * - QUOTE_REQUESTS_DISABLED (403): the shop's live publication has no enabled
25
+ * `quoteRequest` section on an enabled page — it never placed the form, or
26
+ * unpublished it. Nothing is stored.
27
+ * - QUOTE_REQUEST_LIMIT (429): the shop's daily cap for guests, or the hourly
28
+ * cool-down of the guest's address or the signed-in buyer's account, is spent.
29
+ * Nothing is stored.
30
+ * - QUOTE_REQUEST_NOT_FOUND (404): app routes, an unknown `quoteRequestId`.
31
+ * - QUOTE_REQUEST_ALREADY_LINKED (409): `POST /presupuestos` naming a request
32
+ * another Presupuesto already converted. No Presupuesto is written. Also the
33
+ * app `POST /quote-requests { mode: 'status' }` on a converted request: its
34
+ * status belongs to the Presupuesto that converted it and no longer changes.
35
+ */
36
+ export const QUOTE_REQUEST_ERROR_CODES = [
37
+ 'INVALID_QUOTE_REQUEST',
38
+ 'CAPTCHA_INVALID',
39
+ 'ORIGIN_STORE_MISMATCH',
40
+ 'QUOTE_REQUESTS_DISABLED',
41
+ 'QUOTE_REQUEST_LIMIT',
42
+ 'QUOTE_REQUEST_NOT_FOUND',
43
+ 'QUOTE_REQUEST_ALREADY_LINKED',
44
+ ];
45
+ export const QUOTE_REQUEST_STATUSES = ['new', 'in-progress', 'converted', 'dismissed'];
46
+ /** The optional contact fields a `quoteRequest` section may ask for. */
47
+ export const QUOTE_REQUEST_OPTIONAL_FIELDS = ['phone', 'company', 'cuit'];
48
+ /** Bounds of the web write. */
49
+ export const QUOTE_REQUEST_LIMITS = {
50
+ message: 2000,
51
+ items: 50,
52
+ /** The shared base-quantity ceiling (`baseQuantitySchema`), six decimal places. */
53
+ itemQuantity: 1_000_000,
54
+ name: 120,
55
+ company: 120,
56
+ email: 254,
57
+ phone: 40,
58
+ };
59
+ /** The reCAPTCHA action the storefront executes for the web write. */
60
+ export const QUOTE_REQUEST_RECAPTCHA_ACTION = 'quote_request';
@@ -72,7 +72,7 @@ export declare const STOREFRONT_DESIGN_LIMITS: {
72
72
  */
73
73
  export declare const STOREFRONT_PAGE_SLUG_RE: RegExp;
74
74
  export declare const isStorefrontPageSlug: (value: unknown) => value is string;
75
- export declare const STOREFRONT_SECTION_TYPES: readonly ["banner", "imageText", "products", "categories", "brands", "text", "faq", "benefits", "contact", "paymentDelivery", "logoWall", "gallery"];
75
+ export declare const STOREFRONT_SECTION_TYPES: readonly ["banner", "imageText", "products", "categories", "brands", "text", "faq", "benefits", "contact", "paymentDelivery", "logoWall", "gallery", "quoteRequest", "customerRequest"];
76
76
  /**
77
77
  * The section types a page may repeat, each up to `STOREFRONT_DESIGN_LIMITS.repeatableSectionsPerType`
78
78
  * times (disabled sections count), so a Home can show offers and new arrivals together. Every other
@@ -259,6 +259,29 @@ declare global {
259
259
  caption?: string;
260
260
  }[];
261
261
  };
262
+ /**
263
+ * «Pedí un presupuesto»: the storefront form behind web `POST /quote-requests`. `fields` lists
264
+ * the optional contact fields it asks for, each at most once; `allowItems` lets the shopper
265
+ * list products and quantities. Nothing is required to publish. Once per page.
266
+ * The server enforces the placed sections: with no enabled `quoteRequest` section on
267
+ * an enabled published page the write is `403 QUOTE_REQUESTS_DISABLED`; a guest field
268
+ * no placed section lists, or `items` when none sets `allowItems`, is `400`.
269
+ */
270
+ quoteRequest: {
271
+ heading?: string;
272
+ body?: string;
273
+ fields?: QuoteRequestOptionalField[];
274
+ allowItems?: boolean;
275
+ };
276
+ /**
277
+ * «Quiero ser cliente mayorista»: a placement of the wholesale request (`POST /user`
278
+ * `{ mode: 'wholesale-request' }`), never a second intake. Copy only: what it renders is fixed
279
+ * by the shopper's and the shop's state, never by content. Once per page.
280
+ */
281
+ customerRequest: {
282
+ heading?: string;
283
+ body?: string;
284
+ };
262
285
  }
263
286
  type StorefrontDesignAudience = (typeof STOREFRONT_DESIGN_AUDIENCES)[number];
264
287
  /**
@@ -83,6 +83,8 @@ export const STOREFRONT_SECTION_TYPES = [
83
83
  'paymentDelivery',
84
84
  'logoWall',
85
85
  'gallery',
86
+ 'quoteRequest',
87
+ 'customerRequest',
86
88
  ];
87
89
  /**
88
90
  * The section types a page may repeat, each up to `STOREFRONT_DESIGN_LIMITS.repeatableSectionsPerType`
@@ -10,6 +10,13 @@ declare global {
10
10
  screen_type: 'desktop' | 'mobile';
11
11
  app_version: number;
12
12
  ts: string;
13
+ /**
14
+ * Server-stamped on a stored guest event (the unauthenticated `POST /events`
15
+ * ingest) from the request's analytics-consent block, so a later policy change
16
+ * can scope erasure. Never read from a client event: the validators drop it.
17
+ * Distinct from the signup contract's `consentVersion`.
18
+ */
19
+ analyticsConsentVersion?: string;
13
20
  }
14
21
  interface ProductViewedEvent extends StorefrontEventBase {
15
22
  event: 'Product Viewed';
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Guest storefront telemetry ingest — the HTTP route a shopper who never signs
3
+ * in sends first-party storefront events through (the signed-in shopper keeps
4
+ * the WebSocket `logs` action).
5
+ *
6
+ * Web (public, bound origin only): `POST /events` (a batch) and
7
+ * `POST /events/consent` (an analytics-consent withdrawal for one journey).
8
+ *
9
+ * The tenant is the request's bound Origin (a slug host or a verified custom
10
+ * domain), never `x-store` and never an event's `tenant_store_id`. Nothing a
11
+ * guest sends can claim to be a customer: `customer_id` is not part of a guest
12
+ * event, and one that carries it is dropped and counted.
13
+ */
14
+ /**
15
+ * The refusals of the guest ingest routes.
16
+ *
17
+ * - EVENTS_TENANT_UNRESOLVED (403): the request's Origin is bound to no shop.
18
+ * Checked before the body is read; nothing is stored.
19
+ * - EVENTS_BATCH_TOO_LARGE (413): the raw body is over
20
+ * {@link STOREFRONT_INGEST_LIMITS.maxBodyBytes} bytes, or `events` holds more
21
+ * than {@link STOREFRONT_INGEST_LIMITS.maxEvents} entries. Nothing is stored.
22
+ * - VALIDATION_FAILED (400): the request as a WHOLE is malformed — not JSON, not
23
+ * the envelope shape, or (withdrawal) `anonymousId` not a UUID. A single event
24
+ * that does not validate is never a 400: it is a counted drop in `dropped`.
25
+ * - EVENTS_RATE_LIMITED (429): the source's budget is spent. Nothing is stored;
26
+ * retry later.
27
+ */
28
+ export declare const STOREFRONT_INGEST_ERROR_CODES: readonly ["EVENTS_TENANT_UNRESOLVED", "EVENTS_BATCH_TOO_LARGE", "VALIDATION_FAILED", "EVENTS_RATE_LIMITED"];
29
+ /** Bounds of `POST /events`, enforced before any event is read. */
30
+ export declare const STOREFRONT_INGEST_LIMITS: {
31
+ /** The raw request body ceiling, in UTF-8 bytes, measured before parsing. */
32
+ readonly maxBodyBytes: number;
33
+ /** At most this many events per request. */
34
+ readonly maxEvents: 25;
35
+ /** The longest `analyticsConsent.version` accepted; a longer one never stores an event. */
36
+ readonly consentVersionMaxLength: 64;
37
+ };
38
+ declare global {
39
+ type StorefrontIngestErrorCode = (typeof STOREFRONT_INGEST_ERROR_CODES)[number];
40
+ /** The storefront event variants a guest may send. Any other variant is a counted drop. */
41
+ type GuestStorefrontEventName = 'Product Viewed' | 'Product List Viewed' | 'Cart Item Added' | 'Cart Item Removed' | 'Cart Viewed' | 'Search Submitted' | 'Checkout Started' | 'Checkout Step Completed';
42
+ /**
43
+ * A guest event: an allowed variant of `StorefrontEvent` with no
44
+ * `customer_id` (and no server-stamped field). Distributive, so each variant
45
+ * keeps its own discriminated shape.
46
+ */
47
+ type GuestStorefrontEvent = Extract<StorefrontEvent, {
48
+ event: GuestStorefrontEventName;
49
+ }> extends infer Variant ? Variant extends unknown ? Omit<Variant, 'customer_id' | 'audit' | 'analyticsConsentVersion'> : never : never;
50
+ /**
51
+ * The analytics-consent state the storefront holds for this shopper, separate
52
+ * from advertising consent. `version` names the consent text the shopper saw;
53
+ * a persisted event records it as `analyticsConsentVersion`.
54
+ */
55
+ interface StorefrontAnalyticsConsent {
56
+ analytics: 'granted' | 'denied';
57
+ version: string;
58
+ }
59
+ /**
60
+ * Web `POST /events` body. At most 25 events and 32 KiB. With
61
+ * `analytics: 'denied'` no per-journey event is stored — only an aggregate
62
+ * count with no `anonymous_id`. Under the platform's opt-in mode (the
63
+ * default) only `'granted'` stores events, so an absent block stores none;
64
+ * under opt-out everything but `'denied'` stores, an absent block included.
65
+ */
66
+ interface StorefrontEventIngestRequest {
67
+ events: GuestStorefrontEvent[];
68
+ analyticsConsent?: StorefrontAnalyticsConsent;
69
+ }
70
+ /**
71
+ * Web `POST /events` 202 `data`: counts only, never an echo of the batch.
72
+ * `accepted` events were stored (or were already stored under the same
73
+ * `event_id`); `dropped` ones were refused one by one — invalid, a
74
+ * customer-only variant, carrying `customer_id`, another tenant, outside the
75
+ * time window, fenced, not consented, or over a budget.
76
+ */
77
+ interface StorefrontEventIngestResponse {
78
+ accepted: number;
79
+ dropped: number;
80
+ }
81
+ /**
82
+ * Web `POST /events/consent` body: withdraw analytics consent for one
83
+ * journey. Every later event of that `anonymousId` is refused, however late
84
+ * it arrives. Nothing is erased: its earlier events expire by retention
85
+ * (30-day hot rows, the 390-day `guest/` archive). Answers 202 with no data.
86
+ */
87
+ interface StorefrontConsentWithdrawalRequest {
88
+ /** The journey's `anonymous_id` (a UUID). */
89
+ anonymousId: string;
90
+ analytics: 'denied';
91
+ }
92
+ }
93
+ export {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Guest storefront telemetry ingest — the HTTP route a shopper who never signs
3
+ * in sends first-party storefront events through (the signed-in shopper keeps
4
+ * the WebSocket `logs` action).
5
+ *
6
+ * Web (public, bound origin only): `POST /events` (a batch) and
7
+ * `POST /events/consent` (an analytics-consent withdrawal for one journey).
8
+ *
9
+ * The tenant is the request's bound Origin (a slug host or a verified custom
10
+ * domain), never `x-store` and never an event's `tenant_store_id`. Nothing a
11
+ * guest sends can claim to be a customer: `customer_id` is not part of a guest
12
+ * event, and one that carries it is dropped and counted.
13
+ */
14
+ /**
15
+ * The refusals of the guest ingest routes.
16
+ *
17
+ * - EVENTS_TENANT_UNRESOLVED (403): the request's Origin is bound to no shop.
18
+ * Checked before the body is read; nothing is stored.
19
+ * - EVENTS_BATCH_TOO_LARGE (413): the raw body is over
20
+ * {@link STOREFRONT_INGEST_LIMITS.maxBodyBytes} bytes, or `events` holds more
21
+ * than {@link STOREFRONT_INGEST_LIMITS.maxEvents} entries. Nothing is stored.
22
+ * - VALIDATION_FAILED (400): the request as a WHOLE is malformed — not JSON, not
23
+ * the envelope shape, or (withdrawal) `anonymousId` not a UUID. A single event
24
+ * that does not validate is never a 400: it is a counted drop in `dropped`.
25
+ * - EVENTS_RATE_LIMITED (429): the source's budget is spent. Nothing is stored;
26
+ * retry later.
27
+ */
28
+ export const STOREFRONT_INGEST_ERROR_CODES = [
29
+ 'EVENTS_TENANT_UNRESOLVED',
30
+ 'EVENTS_BATCH_TOO_LARGE',
31
+ 'VALIDATION_FAILED',
32
+ 'EVENTS_RATE_LIMITED',
33
+ ];
34
+ /** Bounds of `POST /events`, enforced before any event is read. */
35
+ export const STOREFRONT_INGEST_LIMITS = {
36
+ /** The raw request body ceiling, in UTF-8 bytes, measured before parsing. */
37
+ maxBodyBytes: 32 * 1024,
38
+ /** At most this many events per request. */
39
+ maxEvents: 25,
40
+ /** The longest `analyticsConsent.version` accepted; a longer one never stores an event. */
41
+ consentVersionMaxLength: 64,
42
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platform-types",
3
- "version": "1.10.793",
3
+ "version": "1.10.795",
4
4
  "description": "Shared wire and entity contracts for the platform.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",