platform-types 1.10.777 → 1.10.779

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
@@ -121,6 +121,7 @@ __exportStar(require("./user.js"), exports);
121
121
  __exportStar(require("./userActivity.js"), exports);
122
122
  __exportStar(require("./valuation.js"), exports);
123
123
  __exportStar(require("./webhook.js"), exports);
124
+ __exportStar(require("./wholesaleRequest.js"), exports);
124
125
  __exportStar(require("./wire.js"), exports);
125
126
  __exportStar(require("./workItem.js"), exports);
126
127
  __exportStar(require("./zone.js"), exports);
@@ -6,7 +6,7 @@ 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 do not select a producer subscriber audience;
9
+ // PAYMENT_ATTEMPT_RESOLUTION / CUSTOMER_WHOLESALE_REQUEST do not select a producer subscriber audience;
10
10
  // the consumer still reads the live USER row for each delivery channel.
11
11
  // AFIP_CERT_EXPIRY is seeded on the founding user and uses the subscriber index.
12
12
  // Exported as a real enum consumed by api and app; adding an indexed type
@@ -199,6 +199,24 @@ var NotificationTypeEnum;
199
199
  * re-opens rings again.
200
200
  */
201
201
  NotificationTypeEnum["PAYMENT_ATTEMPT_RESOLUTION"] = "PAYMENT_ATTEMPT_RESOLUTION";
202
+ /**
203
+ * A signed-in storefront buyer asked for a wholesale account
204
+ * (`POST /user { mode: 'wholesale-request' }`) and the request waits in the
205
+ * operator queue (`GET /customers?mode=wholesale-requests`). Names the buyer
206
+ * by name only — never their email or CUIT.
207
+ *
208
+ * ⚠️ ADMIN-ONLY, and its own preference key. The producer fans out to every
209
+ * non-disabled ADMIN of the store (`listActiveAdmins`) — the tier that reads
210
+ * the queue and decides it — so a client offers the
211
+ * `CUSTOMER_WHOLESALE_REQUEST` toggle to ADMINs only. No legacy opt-in scan:
212
+ * the consumer enforces explicit type/channel preferences
213
+ * (`notificationPreferences.CUSTOMER_WHOLESALE_REQUEST`), default-on when
214
+ * absent. Additive — no backfill or consumer migration owed.
215
+ *
216
+ * ⚠️ Carries `wholesaleRequestRef` and NEVER a top-level `customerId` (an
217
+ * audience — the bell would reach the buyer's own storefront socket).
218
+ */
219
+ NotificationTypeEnum["CUSTOMER_WHOLESALE_REQUEST"] = "CUSTOMER_WHOLESALE_REQUEST";
202
220
  })(NotificationTypeEnum || (exports.NotificationTypeEnum = NotificationTypeEnum = {}));
203
221
  /**
204
222
  * The notification types a CUSTOMER bell can carry today; each links through
@@ -691,6 +691,17 @@ exports.SOCKET_ACTIONS = [
691
691
  * where the role and scope gates sit.
692
692
  */
693
693
  'workItems',
694
+ /**
695
+ * BE → the store's ADMIN-TIER operator sockets when a buyer's wholesale
696
+ * request is submitted or decided. Carries the `WholesaleRequest` row as it
697
+ * now stands (company data included), so the queue can patch in place.
698
+ *
699
+ * ⚠️ Admin tier only — `wsPostStore(..., { audience: 'operator', lowTierData:
700
+ * null })`: the queue is an admin-tier read, so a USER or PRINTER socket
701
+ * receives nothing. The buyer follows their own request on the `'customers'`
702
+ * action instead (`WholesaleRequestStatusFrame`).
703
+ */
704
+ 'wholesaleRequests',
694
705
  ];
695
706
  /** Runtime guard — narrows an untrusted string to a known action. */
696
707
  const isSocketAction = (value) => typeof value === 'string' && exports.SOCKET_ACTIONS.includes(value);
package/dist/cjs/store.js CHANGED
@@ -139,6 +139,13 @@ 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
+ * Request refusal, `409`:
143
+ * - `WHOLESALE_LIST_REQUIRED` — an `ecommerce` save turns
144
+ * `config.wholesaleRequests` on while the store holds no
145
+ * `audienceClass: 'wholesale'` list (`store/_ecommercePatch.ts`), with
146
+ * `data: { field: 'ecommerce.config.wholesaleRequests' }`. The platform PUT
147
+ * answers it too.
148
+ *
142
149
  * Maintenance mode (`{ mode: 'maintenance', level }`, `store/_patchMaintenance.ts`):
143
150
  * - `FORBIDDEN` (403) — the caller is neither an ADMIN nor a MANAGER.
144
151
  * - `STORE_NOT_FOUND` (404) — the store row is gone.
@@ -170,6 +177,7 @@ exports.STORE_PATCH_ERROR_CODES = [
170
177
  'DISPLAY_CURRENCY_NOT_INVOICEABLE',
171
178
  'PRODUCT_CURRENCY_NOT_INVOICEABLE',
172
179
  'PRICE_LIST_NOT_FOUND',
180
+ 'WHOLESALE_LIST_REQUIRED',
173
181
  'FORBIDDEN',
174
182
  'STORE_NOT_FOUND',
175
183
  ];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isAdoptableStorefrontLiteral = exports.isAllowedStorefrontLiteralValue = exports.isStorefrontLiteralKey = exports.STOREFRONT_WEB_LITERAL_KEYS = void 0;
3
+ exports.isAdoptableStorefrontLiteral = exports.isStorefrontLiteralKey = exports.STOREFRONT_WEB_LITERAL_KEYS = void 0;
4
4
  const storefrontDesignRegistry_ts_1 = require("./storefrontDesignRegistry.js");
5
5
  /**
6
6
  * Every storefront `WEB` literal key a shop may override, in code-unit order.
@@ -17,6 +17,9 @@ const storefrontDesignRegistry_ts_1 = require("./storefrontDesignRegistry.js");
17
17
  * - keys whose storefront value is not a plain `{name}`-token sentence (it
18
18
  * branches, computes, or is not a string), which a shop string cannot
19
19
  * express.
20
+ * Route paths (the former `NAVIGATE_*` keys) are not copy at all: they live in
21
+ * the storefront's own config, and the api refuses and drops a `NAVIGATE_*`
22
+ * literal on every storefront surface.
20
23
  * An editor that lists any of those offers an edit the server refuses.
21
24
  *
22
25
  * Interpolated values use `{name}` tokens named after the storefront's own
@@ -492,6 +495,8 @@ exports.STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
492
495
  'DATA_RIGHTS_COLLECTION_NOTES',
493
496
  'DATA_RIGHTS_COLLECTION_ORDERS',
494
497
  'DATA_RIGHTS_COLLECTION_PAYMENTS',
498
+ 'DATA_RIGHTS_COLLECTION_RECENT_PURCHASES',
499
+ 'DATA_RIGHTS_COLLECTION_WHOLESALE_REQUESTS',
495
500
  'DATA_RIGHTS_DIALOG_ACK',
496
501
  'DATA_RIGHTS_DIALOG_CONFIRM',
497
502
  'DATA_RIGHTS_DIALOG_FISCAL',
@@ -815,27 +820,6 @@ exports.STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
815
820
  'MOBILE_BUTTONS_FAVORITES',
816
821
  'MOBILE_BUTTONS_INVOICES',
817
822
  'MOBILE_BUTTONS_ORDERS',
818
- 'NAVIGATE_APPOINTMENTS',
819
- 'NAVIGATE_BASKET',
820
- 'NAVIGATE_BRANDS',
821
- 'NAVIGATE_CATEGORIES',
822
- 'NAVIGATE_CHECKOUT',
823
- 'NAVIGATE_FAVORITES',
824
- 'NAVIGATE_HOME',
825
- 'NAVIGATE_INCOMES',
826
- 'NAVIGATE_INVOICES',
827
- 'NAVIGATE_LOGIN',
828
- 'NAVIGATE_LOGOUT',
829
- 'NAVIGATE_MAINTENANCE',
830
- 'NAVIGATE_NEW_ARRIVALS',
831
- 'NAVIGATE_NOTIFICATIONS',
832
- 'NAVIGATE_ORDERS',
833
- 'NAVIGATE_PAYMENTS',
834
- 'NAVIGATE_PROFILE',
835
- 'NAVIGATE_RECOVER',
836
- 'NAVIGATE_REGISTRATION',
837
- 'NAVIGATE_SALE',
838
- 'NAVIGATE_SESSIONS',
839
823
  'NETWORK_OFFLINE',
840
824
  'NETWORK_REQUEST_FAILED',
841
825
  'NETWORK_TIMEOUT',
@@ -1078,6 +1062,7 @@ exports.STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
1078
1062
  'PREVIEW_LOADING',
1079
1063
  'PREVIEW_READ_FAILED',
1080
1064
  'PREVIEW_READ_ONLY',
1065
+ 'PREVIEW_RECENTLY_BOUGHT',
1081
1066
  'PREVIEW_RESTART',
1082
1067
  'PREVIEW_RETRY',
1083
1068
  'PREVIEW_TITLE',
@@ -1346,6 +1331,7 @@ exports.STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
1346
1331
  'STOREFRONT_FOOTER_NAV_ARIA',
1347
1332
  'STOREFRONT_MERCHANT_NAV_ARIA',
1348
1333
  'STOREFRONT_NAV_GROUP_FALLBACK',
1334
+ 'STOREFRONT_PRODUCTS_REFRESH_FAILED',
1349
1335
  'STOREFRONT_SEARCH',
1350
1336
  'STOREFRONT_SOCIAL_FACEBOOK',
1351
1337
  'STOREFRONT_SOCIAL_INSTAGRAM',
@@ -1374,34 +1360,13 @@ const STOREFRONT_WEB_LITERAL_KEY_SET = new Set(exports.STOREFRONT_WEB_LITERAL_KE
1374
1360
  /** Whether a shop may override `key` — membership in {@link STOREFRONT_WEB_LITERAL_KEYS}. */
1375
1361
  const isStorefrontLiteralKey = (key) => STOREFRONT_WEB_LITERAL_KEY_SET.has(key);
1376
1362
  exports.isStorefrontLiteralKey = isStorefrontLiteralKey;
1377
- /** Keys whose value is a storefront route the storefront renders as a link target. */
1378
- const STOREFRONT_NAVIGATE_LITERAL_PREFIX = 'NAVIGATE_';
1379
- /**
1380
- * A same-origin path: a leading `/`, a second character that is neither `/` nor `\`
1381
- * (`//host` and `/\host` are protocol-relative to a browser), and no whitespace,
1382
- * backslash, control character (`\p{Cc}`) or invisible format character (`\p{Cf}`: every
1383
- * bidi mark including U+061C, zero-width characters, the soft hyphen, tag characters)
1384
- * anywhere. Browsers strip tab and newline before parsing, so `/\t/host` becomes `//host`;
1385
- * invisible and directional characters can also disguise the link target. A scheme
1386
- * cannot start with `/`.
1387
- */
1388
- const STOREFRONT_SAME_ORIGIN_PATH_RE = /^\/(?![/\\])[^\s\\\p{Cc}\p{Cf}]*$/u;
1389
- /**
1390
- * Whether `value` may be stored, published or served for `key`. A `NAVIGATE_*` key is
1391
- * rendered as a link, so it must be a same-origin path — otherwise an override could
1392
- * send a shop's customers to an off-site look-alike login page. Every other key is
1393
- * unconstrained here (key membership, well-formedness and length are checked apart).
1394
- */
1395
- const isAllowedStorefrontLiteralValue = (key, value) => !key.startsWith(STOREFRONT_NAVIGATE_LITERAL_PREFIX) || STOREFRONT_SAME_ORIGIN_PATH_RE.test(value);
1396
- exports.isAllowedStorefrontLiteralValue = isAllowedStorefrontLiteralValue;
1397
1363
  /**
1398
1364
  * Whether a shop's stored `value` for `key` is one a save keeps, a read serves and the editor
1399
- * baseline adopts: a shop-overridable key, a well-formed string within
1400
- * {@link STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS} code points, and an allowed value for it.
1365
+ * baseline adopts: a shop-overridable key and a well-formed string within
1366
+ * {@link STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS} code points.
1401
1367
  */
1402
1368
  const isAdoptableStorefrontLiteral = (key, value) => (0, exports.isStorefrontLiteralKey)(key) &&
1403
1369
  typeof value === 'string' &&
1404
1370
  value.isWellFormed() &&
1405
- Array.from(value).length <= storefrontDesignRegistry_ts_1.STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS &&
1406
- (0, exports.isAllowedStorefrontLiteralValue)(key, value);
1371
+ Array.from(value).length <= storefrontDesignRegistry_ts_1.STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS;
1407
1372
  exports.isAdoptableStorefrontLiteral = isAdoptableStorefrontLiteral;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /**
3
+ * A signed-in storefront buyer's request for a wholesale account, and the
4
+ * operator queue that decides it.
5
+ *
6
+ * One row per customer (`WHOLESALEREQ#{storeId}` / `{customerId}`). A new
7
+ * request overwrites the previous one only once that one is decided, so a
8
+ * customer holds at most one pending request.
9
+ *
10
+ * The flow is offered only while the store's
11
+ * `ecommerce.config.wholesaleRequests` is `true` (absent = off).
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.WHOLESALE_REQUEST_ERROR_CODES = exports.WHOLESALE_REQUEST_REASON_MAX = exports.WHOLESALE_REQUEST_PHONE_MAX = exports.WHOLESALE_REQUEST_MESSAGE_MAX = exports.WHOLESALE_REQUEST_ACTIVITY_MAX = exports.WHOLESALE_REQUEST_RAZON_SOCIAL_MAX = exports.WHOLESALE_REQUESTS_PAGE_MAX = exports.WHOLESALE_REQUEST_STATES = void 0;
15
+ /** The three states a request moves through. */
16
+ exports.WHOLESALE_REQUEST_STATES = ['pending', 'approved', 'declined'];
17
+ /** `GET /customers?mode=wholesale-requests` page ceiling; the default page is 50. */
18
+ exports.WHOLESALE_REQUESTS_PAGE_MAX = 100;
19
+ /** Character ceilings on the buyer's free text, counted after trimming. */
20
+ exports.WHOLESALE_REQUEST_RAZON_SOCIAL_MAX = 200;
21
+ exports.WHOLESALE_REQUEST_ACTIVITY_MAX = 120;
22
+ exports.WHOLESALE_REQUEST_MESSAGE_MAX = 1000;
23
+ exports.WHOLESALE_REQUEST_PHONE_MAX = 30;
24
+ /** Character ceiling on the operator's decline reason, counted after trimming. */
25
+ exports.WHOLESALE_REQUEST_REASON_MAX = 500;
26
+ /**
27
+ * Every `error` code the wholesale-request flow answers on its own behalf:
28
+ * - web `POST /user { mode: 'wholesale-request' }` (`stacks/web/lambdas/user/_wholesaleRequest.ts`):
29
+ * `WHOLESALE_REQUESTS_DISABLED`, `CUSTOMER_NOT_FOUND`, `CUSTOMER_ANONYMIZED`, `USER_NOT_ENABLED`,
30
+ * `CUSTOMER_ALREADY_WHOLESALE`, `WHOLESALE_REQUEST_CONTENDED`, `WHOLESALE_REQUEST_PENDING`;
31
+ * - app `GET /customers?mode=wholesale-requests` and `POST /customers { mode: 'wholesale-decision' }`
32
+ * (`stacks/lambdas/customers/_wholesaleRequests.ts`): `ROLE_FORBIDDEN`, `ADMIN_REQUIRED`,
33
+ * `INVALID_CURSOR`, `WHOLESALE_REQUEST_NOT_PENDING`, `WHOLESALE_DECISION_CONTENDED`,
34
+ * `CUSTOMER_NOT_FOUND`, `CUSTOMER_ANONYMIZED`, `CUSTOMER_MERGED`, `PRICE_LIST_NOT_FOUND`,
35
+ * `PRICE_LIST_NOT_WHOLESALE`, `WHOLESALE_REQUEST_NOT_FOUND`;
36
+ * - the opt-in refusal both configuration writers answer when `ecommerce.config.wholesaleRequests`
37
+ * turns on with no wholesale-class list: `WHOLESALE_LIST_REQUIRED` (`409`). It is also in
38
+ * `STORE_PATCH_ERROR_CODES`, on purpose: a wholesale client handles it without reading that tuple.
39
+ *
40
+ * The rule: a code is listed when a wholesale handler writes it as its own literal, and each of
41
+ * those literals is typed `satisfies WholesaleRequestErrorCode`, so one this list misses fails
42
+ * typecheck. A code those routes forward from a shared helper is not listed, though a wholesale
43
+ * route can answer it: the customer-access refusals (`refuseCustomerAccess`), `VALIDATION_FAILED`
44
+ * (`validateRequest`), the handler wrapper's own codes and the authorizer's.
45
+ */
46
+ exports.WHOLESALE_REQUEST_ERROR_CODES = [
47
+ 'WHOLESALE_REQUESTS_DISABLED',
48
+ 'CUSTOMER_NOT_FOUND',
49
+ 'CUSTOMER_ANONYMIZED',
50
+ 'USER_NOT_ENABLED',
51
+ 'CUSTOMER_ALREADY_WHOLESALE',
52
+ 'WHOLESALE_REQUEST_CONTENDED',
53
+ 'WHOLESALE_REQUEST_PENDING',
54
+ 'ROLE_FORBIDDEN',
55
+ 'ADMIN_REQUIRED',
56
+ 'INVALID_CURSOR',
57
+ 'WHOLESALE_REQUEST_NOT_PENDING',
58
+ 'WHOLESALE_DECISION_CONTENDED',
59
+ 'CUSTOMER_MERGED',
60
+ 'PRICE_LIST_NOT_FOUND',
61
+ 'PRICE_LIST_NOT_WHOLESALE',
62
+ 'WHOLESALE_REQUEST_NOT_FOUND',
63
+ 'WHOLESALE_LIST_REQUIRED',
64
+ ];
@@ -1238,6 +1238,15 @@ declare global {
1238
1238
  * On BOTH surfaces; `[]` when the subject has none.
1239
1239
  */
1240
1240
  recentPurchases: CustomerRecentPurchaseExportEntry[];
1241
+ /**
1242
+ * The subject's wholesale-account requests, on BOTH surfaces: the one filed
1243
+ * under their own id plus any filed under a merged-away id folded into them
1244
+ * (the same folded ids, and the same cap, as the financial collections) —
1245
+ * the company data submitted and its state, never the decline reason or the
1246
+ * deciding operator. Absent when there is none (and from servers predating
1247
+ * the request flow).
1248
+ */
1249
+ wholesaleRequests?: WholesaleRequestExportEntry[];
1241
1250
  /**
1242
1251
  * Per COLLECTION, and honest: `true` means the read stopped before the
1243
1252
  * end of a partition, folded financial IDs were omitted at the cap, or whole
@@ -1261,6 +1270,12 @@ declare global {
1261
1270
  favorites: boolean;
1262
1271
  /** `true` when folded ids past the cap were not read. */
1263
1272
  recentPurchases: boolean;
1273
+ /**
1274
+ * Present when `wholesaleRequests` is, or when merged-away ids were cut
1275
+ * at the folded-id cap — then `true`: a request filed under an omitted id
1276
+ * may be missing.
1277
+ */
1278
+ wholesaleRequests?: boolean;
1264
1279
  };
1265
1280
  }
1266
1281
  /**
package/dist/index.d.ts CHANGED
@@ -97,6 +97,7 @@ export * from './user.ts';
97
97
  export * from './userActivity.ts';
98
98
  export * from './valuation.ts';
99
99
  export * from './webhook.ts';
100
+ export * from './wholesaleRequest.ts';
100
101
  export * from './wire.ts';
101
102
  export * from './workItem.ts';
102
103
  export * from './zone.ts';
package/dist/index.js CHANGED
@@ -105,6 +105,7 @@ export * from "./user.js";
105
105
  export * from "./userActivity.js";
106
106
  export * from "./valuation.js";
107
107
  export * from "./webhook.js";
108
+ export * from "./wholesaleRequest.js";
108
109
  export * from "./wire.js";
109
110
  export * from "./workItem.js";
110
111
  export * from "./zone.js";
@@ -177,7 +177,25 @@ export declare enum NotificationTypeEnum {
177
177
  * nothing. An attempt an operator closed (`resolved`) and a later approval
178
178
  * re-opens rings again.
179
179
  */
180
- PAYMENT_ATTEMPT_RESOLUTION = "PAYMENT_ATTEMPT_RESOLUTION"
180
+ PAYMENT_ATTEMPT_RESOLUTION = "PAYMENT_ATTEMPT_RESOLUTION",
181
+ /**
182
+ * A signed-in storefront buyer asked for a wholesale account
183
+ * (`POST /user { mode: 'wholesale-request' }`) and the request waits in the
184
+ * operator queue (`GET /customers?mode=wholesale-requests`). Names the buyer
185
+ * by name only — never their email or CUIT.
186
+ *
187
+ * ⚠️ ADMIN-ONLY, and its own preference key. The producer fans out to every
188
+ * non-disabled ADMIN of the store (`listActiveAdmins`) — the tier that reads
189
+ * the queue and decides it — so a client offers the
190
+ * `CUSTOMER_WHOLESALE_REQUEST` toggle to ADMINs only. No legacy opt-in scan:
191
+ * the consumer enforces explicit type/channel preferences
192
+ * (`notificationPreferences.CUSTOMER_WHOLESALE_REQUEST`), default-on when
193
+ * absent. Additive — no backfill or consumer migration owed.
194
+ *
195
+ * ⚠️ Carries `wholesaleRequestRef` and NEVER a top-level `customerId` (an
196
+ * audience — the bell would reach the buyer's own storefront socket).
197
+ */
198
+ CUSTOMER_WHOLESALE_REQUEST = "CUSTOMER_WHOLESALE_REQUEST"
181
199
  }
182
200
  /**
183
201
  * The notification types a CUSTOMER bell can carry today; each links through
@@ -290,6 +308,11 @@ declare global {
290
308
  * POINTER, not an audience — see {@link PendingCustomerRef}.
291
309
  */
292
310
  pendingCustomerRef?: PendingCustomerRef;
311
+ /**
312
+ * `CUSTOMER_WHOLESALE_REQUEST` only: which request to open. A POINTER, not
313
+ * an audience — see {@link WholesaleRequestRef}.
314
+ */
315
+ wholesaleRequestRef?: WholesaleRequestRef;
293
316
  /** `MENTION` only: the colleague who wrote the mention (a snapshot). */
294
317
  actor?: NotificationActor;
295
318
  userId?: string;
@@ -353,7 +376,7 @@ declare global {
353
376
  * customer socket carry — the stored row minus the operator-only pointers.
354
377
  * A union keyed on `type`; `ORDER` is its only member today.
355
378
  */
356
- type CustomerNotification = Omit<NotificationInterface, 'cartRef' | 'pendingCustomerRef' | 'TableName' | 'type' | 'orderId'> & {
379
+ type CustomerNotification = Omit<NotificationInterface, 'cartRef' | 'pendingCustomerRef' | 'wholesaleRequestRef' | 'TableName' | 'type' | 'orderId'> & {
357
380
  type: NotificationTypeEnum.ORDER;
358
381
  /** Set on every customer row: the order the bell is about. */
359
382
  orderId: string;
@@ -3,7 +3,7 @@
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 do not select a producer subscriber audience;
6
+ // PAYMENT_ATTEMPT_RESOLUTION / CUSTOMER_WHOLESALE_REQUEST do not select a producer subscriber audience;
7
7
  // the consumer still reads the live USER row for each delivery channel.
8
8
  // AFIP_CERT_EXPIRY is seeded on the founding user and uses the subscriber index.
9
9
  // Exported as a real enum consumed by api and app; adding an indexed type
@@ -196,6 +196,24 @@ export var NotificationTypeEnum;
196
196
  * re-opens rings again.
197
197
  */
198
198
  NotificationTypeEnum["PAYMENT_ATTEMPT_RESOLUTION"] = "PAYMENT_ATTEMPT_RESOLUTION";
199
+ /**
200
+ * A signed-in storefront buyer asked for a wholesale account
201
+ * (`POST /user { mode: 'wholesale-request' }`) and the request waits in the
202
+ * operator queue (`GET /customers?mode=wholesale-requests`). Names the buyer
203
+ * by name only — never their email or CUIT.
204
+ *
205
+ * ⚠️ ADMIN-ONLY, and its own preference key. The producer fans out to every
206
+ * non-disabled ADMIN of the store (`listActiveAdmins`) — the tier that reads
207
+ * the queue and decides it — so a client offers the
208
+ * `CUSTOMER_WHOLESALE_REQUEST` toggle to ADMINs only. No legacy opt-in scan:
209
+ * the consumer enforces explicit type/channel preferences
210
+ * (`notificationPreferences.CUSTOMER_WHOLESALE_REQUEST`), default-on when
211
+ * absent. Additive — no backfill or consumer migration owed.
212
+ *
213
+ * ⚠️ Carries `wholesaleRequestRef` and NEVER a top-level `customerId` (an
214
+ * audience — the bell would reach the buyer's own storefront socket).
215
+ */
216
+ NotificationTypeEnum["CUSTOMER_WHOLESALE_REQUEST"] = "CUSTOMER_WHOLESALE_REQUEST";
199
217
  })(NotificationTypeEnum || (NotificationTypeEnum = {}));
200
218
  /**
201
219
  * The notification types a CUSTOMER bell can carry today; each links through
package/dist/socket.d.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  * A client must ignore actions it does not own rather than assume it
27
27
  * receives all of them.
28
28
  */
29
- export declare const SOCKET_ACTIONS: readonly ["account", "appointments", "appointmentWaitlist", "attendance", "attributes", "automation-rules", "baskets", "cart", "brands", "cash", "catalogOperation", "categories", "campaigns", "customers", "customerNote", "customerAddress", "customerContact", "deliveries", "globals", "inventory", "invoices", "literals", "loyalty", "orders", "presupuestos", "products", "promotions", "segments", "returns", "rma", "templates", "services", "service-templates", "shifts", "stores", "suppliers", "supplier-invoice", "support", "surveys", "users", "warehouses", "zones", "favorites", "webhooks", "account-delete", "baskets-delete", "cart-delete", "log-delete", "notifications", "support-message", "payment_received", "mp_payment_status_changed", "payment_linked", "payment_unlinked", "mercadopago", "mercadopago_dynamic_qr_created", "mercadopago_static_qr_created", "mp_hook_log_appended", "mp_ipn_log_appended", "orderPaymentAttempt", "orderDelivery", "paymentAttemptChanged", "stripe", "subscription", "caea", "print", "print-order", "print-invoice", "print-product", "print-tag", "print_job_failed", "request_printers", "printers_changed", "agent_status_changed", "print_rules_changed", "printers_active", "agent_command", "maintenance", "currency_auto_updated", "drain_progress", "integration_event_appended", "userActivityRecorded", "mercadolibre_settlement_period", "customDomain", "purchaseOrders", "cycleCounts", "stockTransfers", "workItems"];
29
+ export declare const SOCKET_ACTIONS: readonly ["account", "appointments", "appointmentWaitlist", "attendance", "attributes", "automation-rules", "baskets", "cart", "brands", "cash", "catalogOperation", "categories", "campaigns", "customers", "customerNote", "customerAddress", "customerContact", "deliveries", "globals", "inventory", "invoices", "literals", "loyalty", "orders", "presupuestos", "products", "promotions", "segments", "returns", "rma", "templates", "services", "service-templates", "shifts", "stores", "suppliers", "supplier-invoice", "support", "surveys", "users", "warehouses", "zones", "favorites", "webhooks", "account-delete", "baskets-delete", "cart-delete", "log-delete", "notifications", "support-message", "payment_received", "mp_payment_status_changed", "payment_linked", "payment_unlinked", "mercadopago", "mercadopago_dynamic_qr_created", "mercadopago_static_qr_created", "mp_hook_log_appended", "mp_ipn_log_appended", "orderPaymentAttempt", "orderDelivery", "paymentAttemptChanged", "stripe", "subscription", "caea", "print", "print-order", "print-invoice", "print-product", "print-tag", "print_job_failed", "request_printers", "printers_changed", "agent_status_changed", "print_rules_changed", "printers_active", "agent_command", "maintenance", "currency_auto_updated", "drain_progress", "integration_event_appended", "userActivityRecorded", "mercadolibre_settlement_period", "customDomain", "purchaseOrders", "cycleCounts", "stockTransfers", "workItems", "wholesaleRequests"];
30
30
  /** Union of every server→client data-frame action. */
31
31
  export type SocketAction = (typeof SOCKET_ACTIONS)[number];
32
32
  /**
package/dist/socket.js CHANGED
@@ -688,6 +688,17 @@ export const SOCKET_ACTIONS = [
688
688
  * where the role and scope gates sit.
689
689
  */
690
690
  'workItems',
691
+ /**
692
+ * BE → the store's ADMIN-TIER operator sockets when a buyer's wholesale
693
+ * request is submitted or decided. Carries the `WholesaleRequest` row as it
694
+ * now stands (company data included), so the queue can patch in place.
695
+ *
696
+ * ⚠️ Admin tier only — `wsPostStore(..., { audience: 'operator', lowTierData:
697
+ * null })`: the queue is an admin-tier read, so a USER or PRINTER socket
698
+ * receives nothing. The buyer follows their own request on the `'customers'`
699
+ * action instead (`WholesaleRequestStatusFrame`).
700
+ */
701
+ 'wholesaleRequests',
691
702
  ];
692
703
  /** Runtime guard — narrows an untrusted string to a known action. */
693
704
  export const isSocketAction = (value) => typeof value === 'string' && SOCKET_ACTIONS.includes(value);
package/dist/store.d.ts CHANGED
@@ -146,6 +146,18 @@ declare global {
146
146
  * frame), so the storefront can say the account needs approval.
147
147
  */
148
148
  registrationMode?: 'open' | 'approval';
149
+ /**
150
+ * Whether a signed-in storefront buyer may ask for a wholesale account
151
+ * (`POST /user { mode: 'wholesale-request' }`). Absent or `false` = off:
152
+ * the request route refuses `403 WHOLESALE_REQUESTS_DISABLED`. Explicit on
153
+ * purpose — a store may hold wholesale-class lists for customers it assigns
154
+ * itself without inviting requests. Turning it on requires at least one
155
+ * `audienceClass: 'wholesale'` list (`409 WHOLESALE_LIST_REQUIRED`).
156
+ * Written by `PATCH /store` (ADMIN) and `PUT /platform/stores/{storeId}`
157
+ * (MANAGER). SENT TO SHOPPERS (web `GET /store` and the customer `stores`
158
+ * frame), so the storefront can offer the request.
159
+ */
160
+ wholesaleRequests?: boolean;
149
161
  /**
150
162
  * How long a storefront online payment holds the order, in whole
151
163
  * minutes: the payment attempt's `expiresAt` and its Checkout Pro
@@ -198,9 +210,11 @@ declare global {
198
210
  /** Sparse tenant PATCH shape; live null values reset to registry defaults and are never stored.
199
211
  * An unknown newCustomerPriceList refuses 400 PRICE_LIST_NOT_FOUND with
200
212
  * data: { field: 'ecommerce.config.newCustomerPriceList', value: number }; nothing written.
213
+ * `wholesaleRequests: true` on a store with no `audienceClass: 'wholesale'` list
214
+ * refuses 409 WHOLESALE_LIST_REQUIRED; nothing written.
201
215
  */
202
216
  interface EcommercePatchInput {
203
- config?: Pick<NonNullable<Ecommerce['config']>, 'showDefaultPriceList' | 'newCustomerPriceList' | 'registrationMode' | 'paymentHoldMinutes'> & StorefrontLiveSettingsPatch;
217
+ config?: Pick<NonNullable<Ecommerce['config']>, 'showDefaultPriceList' | 'newCustomerPriceList' | 'registrationMode' | 'wholesaleRequests' | 'paymentHoldMinutes'> & StorefrontLiveSettingsPatch;
204
218
  sidebar?: Ecommerce['sidebar'];
205
219
  home?: Omit<NonNullable<Ecommerce['home']>, 'displayOrders' | 'displayProfile'>;
206
220
  themeColors?: Ecommerce['themeColors'];
@@ -2398,6 +2412,13 @@ export type StoreModuleRefusalBody = {
2398
2412
  * entry naming no list of the store, with `data: { field, value }` where
2399
2413
  * `field` is `<catalog>[<row>].availability.priceLists[<entry>]`.
2400
2414
  *
2415
+ * Request refusal, `409`:
2416
+ * - `WHOLESALE_LIST_REQUIRED` — an `ecommerce` save turns
2417
+ * `config.wholesaleRequests` on while the store holds no
2418
+ * `audienceClass: 'wholesale'` list (`store/_ecommercePatch.ts`), with
2419
+ * `data: { field: 'ecommerce.config.wholesaleRequests' }`. The platform PUT
2420
+ * answers it too.
2421
+ *
2401
2422
  * Maintenance mode (`{ mode: 'maintenance', level }`, `store/_patchMaintenance.ts`):
2402
2423
  * - `FORBIDDEN` (403) — the caller is neither an ADMIN nor a MANAGER.
2403
2424
  * - `STORE_NOT_FOUND` (404) — the store row is gone.
@@ -2411,5 +2432,5 @@ export type StoreModuleRefusalBody = {
2411
2432
  * checks, the display-currency lock, the storefront fence) are not listed
2412
2433
  * either. Cross-cutting refusals: check `GATING_ERROR_CODES` as well.
2413
2434
  */
2414
- 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", "FORBIDDEN", "STORE_NOT_FOUND"];
2435
+ 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"];
2415
2436
  export type StorePatchErrorCode = (typeof STORE_PATCH_ERROR_CODES)[number];
package/dist/store.js CHANGED
@@ -134,6 +134,13 @@ 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
+ * Request refusal, `409`:
138
+ * - `WHOLESALE_LIST_REQUIRED` — an `ecommerce` save turns
139
+ * `config.wholesaleRequests` on while the store holds no
140
+ * `audienceClass: 'wholesale'` list (`store/_ecommercePatch.ts`), with
141
+ * `data: { field: 'ecommerce.config.wholesaleRequests' }`. The platform PUT
142
+ * answers it too.
143
+ *
137
144
  * Maintenance mode (`{ mode: 'maintenance', level }`, `store/_patchMaintenance.ts`):
138
145
  * - `FORBIDDEN` (403) — the caller is neither an ADMIN nor a MANAGER.
139
146
  * - `STORE_NOT_FOUND` (404) — the store row is gone.
@@ -165,6 +172,7 @@ export const STORE_PATCH_ERROR_CODES = [
165
172
  'DISPLAY_CURRENCY_NOT_INVOICEABLE',
166
173
  'PRODUCT_CURRENCY_NOT_INVOICEABLE',
167
174
  'PRICE_LIST_NOT_FOUND',
175
+ 'WHOLESALE_LIST_REQUIRED',
168
176
  'FORBIDDEN',
169
177
  'STORE_NOT_FOUND',
170
178
  ];
@@ -13,6 +13,9 @@
13
13
  * - keys whose storefront value is not a plain `{name}`-token sentence (it
14
14
  * branches, computes, or is not a string), which a shop string cannot
15
15
  * express.
16
+ * Route paths (the former `NAVIGATE_*` keys) are not copy at all: they live in
17
+ * the storefront's own config, and the api refuses and drops a `NAVIGATE_*`
18
+ * literal on every storefront surface.
16
19
  * An editor that lists any of those offers an edit the server refuses.
17
20
  *
18
21
  * Interpolated values use `{name}` tokens named after the storefront's own
@@ -21,16 +24,9 @@
21
24
  export declare const STOREFRONT_WEB_LITERAL_KEYS: readonly string[];
22
25
  /** Whether a shop may override `key` — membership in {@link STOREFRONT_WEB_LITERAL_KEYS}. */
23
26
  export declare const isStorefrontLiteralKey: (key: string) => boolean;
24
- /**
25
- * Whether `value` may be stored, published or served for `key`. A `NAVIGATE_*` key is
26
- * rendered as a link, so it must be a same-origin path — otherwise an override could
27
- * send a shop's customers to an off-site look-alike login page. Every other key is
28
- * unconstrained here (key membership, well-formedness and length are checked apart).
29
- */
30
- export declare const isAllowedStorefrontLiteralValue: (key: string, value: string) => boolean;
31
27
  /**
32
28
  * Whether a shop's stored `value` for `key` is one a save keeps, a read serves and the editor
33
- * baseline adopts: a shop-overridable key, a well-formed string within
34
- * {@link STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS} code points, and an allowed value for it.
29
+ * baseline adopts: a shop-overridable key and a well-formed string within
30
+ * {@link STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS} code points.
35
31
  */
36
32
  export declare const isAdoptableStorefrontLiteral: (key: string, value: unknown) => value is string;
@@ -14,6 +14,9 @@ import { STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS } from "./storefrontDesignReg
14
14
  * - keys whose storefront value is not a plain `{name}`-token sentence (it
15
15
  * branches, computes, or is not a string), which a shop string cannot
16
16
  * express.
17
+ * Route paths (the former `NAVIGATE_*` keys) are not copy at all: they live in
18
+ * the storefront's own config, and the api refuses and drops a `NAVIGATE_*`
19
+ * literal on every storefront surface.
17
20
  * An editor that lists any of those offers an edit the server refuses.
18
21
  *
19
22
  * Interpolated values use `{name}` tokens named after the storefront's own
@@ -489,6 +492,8 @@ export const STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
489
492
  'DATA_RIGHTS_COLLECTION_NOTES',
490
493
  'DATA_RIGHTS_COLLECTION_ORDERS',
491
494
  'DATA_RIGHTS_COLLECTION_PAYMENTS',
495
+ 'DATA_RIGHTS_COLLECTION_RECENT_PURCHASES',
496
+ 'DATA_RIGHTS_COLLECTION_WHOLESALE_REQUESTS',
492
497
  'DATA_RIGHTS_DIALOG_ACK',
493
498
  'DATA_RIGHTS_DIALOG_CONFIRM',
494
499
  'DATA_RIGHTS_DIALOG_FISCAL',
@@ -812,27 +817,6 @@ export const STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
812
817
  'MOBILE_BUTTONS_FAVORITES',
813
818
  'MOBILE_BUTTONS_INVOICES',
814
819
  'MOBILE_BUTTONS_ORDERS',
815
- 'NAVIGATE_APPOINTMENTS',
816
- 'NAVIGATE_BASKET',
817
- 'NAVIGATE_BRANDS',
818
- 'NAVIGATE_CATEGORIES',
819
- 'NAVIGATE_CHECKOUT',
820
- 'NAVIGATE_FAVORITES',
821
- 'NAVIGATE_HOME',
822
- 'NAVIGATE_INCOMES',
823
- 'NAVIGATE_INVOICES',
824
- 'NAVIGATE_LOGIN',
825
- 'NAVIGATE_LOGOUT',
826
- 'NAVIGATE_MAINTENANCE',
827
- 'NAVIGATE_NEW_ARRIVALS',
828
- 'NAVIGATE_NOTIFICATIONS',
829
- 'NAVIGATE_ORDERS',
830
- 'NAVIGATE_PAYMENTS',
831
- 'NAVIGATE_PROFILE',
832
- 'NAVIGATE_RECOVER',
833
- 'NAVIGATE_REGISTRATION',
834
- 'NAVIGATE_SALE',
835
- 'NAVIGATE_SESSIONS',
836
820
  'NETWORK_OFFLINE',
837
821
  'NETWORK_REQUEST_FAILED',
838
822
  'NETWORK_TIMEOUT',
@@ -1075,6 +1059,7 @@ export const STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
1075
1059
  'PREVIEW_LOADING',
1076
1060
  'PREVIEW_READ_FAILED',
1077
1061
  'PREVIEW_READ_ONLY',
1062
+ 'PREVIEW_RECENTLY_BOUGHT',
1078
1063
  'PREVIEW_RESTART',
1079
1064
  'PREVIEW_RETRY',
1080
1065
  'PREVIEW_TITLE',
@@ -1343,6 +1328,7 @@ export const STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
1343
1328
  'STOREFRONT_FOOTER_NAV_ARIA',
1344
1329
  'STOREFRONT_MERCHANT_NAV_ARIA',
1345
1330
  'STOREFRONT_NAV_GROUP_FALLBACK',
1331
+ 'STOREFRONT_PRODUCTS_REFRESH_FAILED',
1346
1332
  'STOREFRONT_SEARCH',
1347
1333
  'STOREFRONT_SOCIAL_FACEBOOK',
1348
1334
  'STOREFRONT_SOCIAL_INSTAGRAM',
@@ -1370,32 +1356,12 @@ export const STOREFRONT_WEB_LITERAL_KEYS = Object.freeze([
1370
1356
  const STOREFRONT_WEB_LITERAL_KEY_SET = new Set(STOREFRONT_WEB_LITERAL_KEYS);
1371
1357
  /** Whether a shop may override `key` — membership in {@link STOREFRONT_WEB_LITERAL_KEYS}. */
1372
1358
  export const isStorefrontLiteralKey = (key) => STOREFRONT_WEB_LITERAL_KEY_SET.has(key);
1373
- /** Keys whose value is a storefront route the storefront renders as a link target. */
1374
- const STOREFRONT_NAVIGATE_LITERAL_PREFIX = 'NAVIGATE_';
1375
- /**
1376
- * A same-origin path: a leading `/`, a second character that is neither `/` nor `\`
1377
- * (`//host` and `/\host` are protocol-relative to a browser), and no whitespace,
1378
- * backslash, control character (`\p{Cc}`) or invisible format character (`\p{Cf}`: every
1379
- * bidi mark including U+061C, zero-width characters, the soft hyphen, tag characters)
1380
- * anywhere. Browsers strip tab and newline before parsing, so `/\t/host` becomes `//host`;
1381
- * invisible and directional characters can also disguise the link target. A scheme
1382
- * cannot start with `/`.
1383
- */
1384
- const STOREFRONT_SAME_ORIGIN_PATH_RE = /^\/(?![/\\])[^\s\\\p{Cc}\p{Cf}]*$/u;
1385
- /**
1386
- * Whether `value` may be stored, published or served for `key`. A `NAVIGATE_*` key is
1387
- * rendered as a link, so it must be a same-origin path — otherwise an override could
1388
- * send a shop's customers to an off-site look-alike login page. Every other key is
1389
- * unconstrained here (key membership, well-formedness and length are checked apart).
1390
- */
1391
- export const isAllowedStorefrontLiteralValue = (key, value) => !key.startsWith(STOREFRONT_NAVIGATE_LITERAL_PREFIX) || STOREFRONT_SAME_ORIGIN_PATH_RE.test(value);
1392
1359
  /**
1393
1360
  * Whether a shop's stored `value` for `key` is one a save keeps, a read serves and the editor
1394
- * baseline adopts: a shop-overridable key, a well-formed string within
1395
- * {@link STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS} code points, and an allowed value for it.
1361
+ * baseline adopts: a shop-overridable key and a well-formed string within
1362
+ * {@link STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS} code points.
1396
1363
  */
1397
1364
  export const isAdoptableStorefrontLiteral = (key, value) => isStorefrontLiteralKey(key) &&
1398
1365
  typeof value === 'string' &&
1399
1366
  value.isWellFormed() &&
1400
- Array.from(value).length <= STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS &&
1401
- isAllowedStorefrontLiteralValue(key, value);
1367
+ Array.from(value).length <= STOREFRONT_DESIGN_LITERAL_MAX_CODE_POINTS;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * A signed-in storefront buyer's request for a wholesale account, and the
3
+ * operator queue that decides it.
4
+ *
5
+ * One row per customer (`WHOLESALEREQ#{storeId}` / `{customerId}`). A new
6
+ * request overwrites the previous one only once that one is decided, so a
7
+ * customer holds at most one pending request.
8
+ *
9
+ * The flow is offered only while the store's
10
+ * `ecommerce.config.wholesaleRequests` is `true` (absent = off).
11
+ */
12
+ /** The three states a request moves through. */
13
+ export declare const WHOLESALE_REQUEST_STATES: readonly ["pending", "approved", "declined"];
14
+ /** `GET /customers?mode=wholesale-requests` page ceiling; the default page is 50. */
15
+ export declare const WHOLESALE_REQUESTS_PAGE_MAX = 100;
16
+ /** Character ceilings on the buyer's free text, counted after trimming. */
17
+ export declare const WHOLESALE_REQUEST_RAZON_SOCIAL_MAX = 200;
18
+ export declare const WHOLESALE_REQUEST_ACTIVITY_MAX = 120;
19
+ export declare const WHOLESALE_REQUEST_MESSAGE_MAX = 1000;
20
+ export declare const WHOLESALE_REQUEST_PHONE_MAX = 30;
21
+ /** Character ceiling on the operator's decline reason, counted after trimming. */
22
+ export declare const WHOLESALE_REQUEST_REASON_MAX = 500;
23
+ /**
24
+ * Every `error` code the wholesale-request flow answers on its own behalf:
25
+ * - web `POST /user { mode: 'wholesale-request' }` (`stacks/web/lambdas/user/_wholesaleRequest.ts`):
26
+ * `WHOLESALE_REQUESTS_DISABLED`, `CUSTOMER_NOT_FOUND`, `CUSTOMER_ANONYMIZED`, `USER_NOT_ENABLED`,
27
+ * `CUSTOMER_ALREADY_WHOLESALE`, `WHOLESALE_REQUEST_CONTENDED`, `WHOLESALE_REQUEST_PENDING`;
28
+ * - app `GET /customers?mode=wholesale-requests` and `POST /customers { mode: 'wholesale-decision' }`
29
+ * (`stacks/lambdas/customers/_wholesaleRequests.ts`): `ROLE_FORBIDDEN`, `ADMIN_REQUIRED`,
30
+ * `INVALID_CURSOR`, `WHOLESALE_REQUEST_NOT_PENDING`, `WHOLESALE_DECISION_CONTENDED`,
31
+ * `CUSTOMER_NOT_FOUND`, `CUSTOMER_ANONYMIZED`, `CUSTOMER_MERGED`, `PRICE_LIST_NOT_FOUND`,
32
+ * `PRICE_LIST_NOT_WHOLESALE`, `WHOLESALE_REQUEST_NOT_FOUND`;
33
+ * - the opt-in refusal both configuration writers answer when `ecommerce.config.wholesaleRequests`
34
+ * turns on with no wholesale-class list: `WHOLESALE_LIST_REQUIRED` (`409`). It is also in
35
+ * `STORE_PATCH_ERROR_CODES`, on purpose: a wholesale client handles it without reading that tuple.
36
+ *
37
+ * The rule: a code is listed when a wholesale handler writes it as its own literal, and each of
38
+ * those literals is typed `satisfies WholesaleRequestErrorCode`, so one this list misses fails
39
+ * typecheck. A code those routes forward from a shared helper is not listed, though a wholesale
40
+ * route can answer it: the customer-access refusals (`refuseCustomerAccess`), `VALIDATION_FAILED`
41
+ * (`validateRequest`), the handler wrapper's own codes and the authorizer's.
42
+ */
43
+ export declare const WHOLESALE_REQUEST_ERROR_CODES: readonly ["WHOLESALE_REQUESTS_DISABLED", "CUSTOMER_NOT_FOUND", "CUSTOMER_ANONYMIZED", "USER_NOT_ENABLED", "CUSTOMER_ALREADY_WHOLESALE", "WHOLESALE_REQUEST_CONTENDED", "WHOLESALE_REQUEST_PENDING", "ROLE_FORBIDDEN", "ADMIN_REQUIRED", "INVALID_CURSOR", "WHOLESALE_REQUEST_NOT_PENDING", "WHOLESALE_DECISION_CONTENDED", "CUSTOMER_MERGED", "PRICE_LIST_NOT_FOUND", "PRICE_LIST_NOT_WHOLESALE", "WHOLESALE_REQUEST_NOT_FOUND", "WHOLESALE_LIST_REQUIRED"];
44
+ export type WholesaleRequestErrorCode = (typeof WHOLESALE_REQUEST_ERROR_CODES)[number];
45
+ declare global {
46
+ type WholesaleRequestState = (typeof WHOLESALE_REQUEST_STATES)[number];
47
+ /**
48
+ * The company data a buyer submits. `cuit`, `razonSocial` and `condFiscal`
49
+ * are required; the rest is optional.
50
+ *
51
+ * ⚠️ PII under Ley 25.326. It reaches the operator queue and the buyer's own
52
+ * data export, and never a bell, a log line or an ERROR row.
53
+ */
54
+ interface WholesaleRequestCompany {
55
+ /** 11 digits with a valid check digit, stored digits-only. */
56
+ cuit: string;
57
+ razonSocial: string;
58
+ /** A `condFiscal` code from `FISCAL_CONDITIONS`. */
59
+ condFiscal: number;
60
+ activity?: string;
61
+ phone?: string;
62
+ message?: string;
63
+ }
64
+ /** Web `POST /user` body that submits a request. */
65
+ interface WholesaleRequestInput extends WholesaleRequestCompany {
66
+ mode: 'wholesale-request';
67
+ }
68
+ /**
69
+ * What the BUYER sees of their request: web `GET /user?mode=wholesale-request`
70
+ * (`data`, or `data: null` when they never asked), the submit's answer and
71
+ * the status frame. Never the decline reason, the deciding operator or the
72
+ * list it moved them to.
73
+ */
74
+ interface WholesaleRequestStatus {
75
+ status: WholesaleRequestState;
76
+ /** Unix ms the request was submitted. */
77
+ createdAt: number;
78
+ /** Unix ms of the decision. Absent while pending. */
79
+ decidedAt?: number;
80
+ }
81
+ /** The operator's view of one request (admin tier only). */
82
+ interface WholesaleRequest extends WholesaleRequestCompany {
83
+ customerId: string;
84
+ status: WholesaleRequestState;
85
+ createdAt: number;
86
+ updatedAt?: number;
87
+ decidedAt?: number;
88
+ /** The deciding operator's user id. */
89
+ decidedBy?: string;
90
+ /** On approval: the wholesale-class list the customer was moved to. */
91
+ priceList?: number;
92
+ /** On decline: the operator's note. Operator-only, never sent to the buyer. */
93
+ reason?: string;
94
+ }
95
+ /**
96
+ * Where the requesting customer stands now, read off the CUSTOMER row when
97
+ * the queue page is served. A request whose customer is gone is listed with
98
+ * that state, never dropped.
99
+ */
100
+ type WholesaleRequestCustomerState = 'active' | 'disabled' | 'missing' | 'anonymized' | 'merged';
101
+ /** One `GET /customers?mode=wholesale-requests` item. */
102
+ interface WholesaleRequestQueueEntry extends WholesaleRequest {
103
+ customer: {
104
+ state: WholesaleRequestCustomerState;
105
+ fullName?: string;
106
+ /** The customer's CURRENT stored list. */
107
+ priceList?: number;
108
+ };
109
+ }
110
+ /**
111
+ * `GET /customers?mode=wholesale-requests` answer. `LastEvaluatedKey` is an
112
+ * opaque cursor, sent back as `?startKey=`; present (with `truncated: true`)
113
+ * only while more rows may follow.
114
+ */
115
+ interface WholesaleRequestsQueueResponse {
116
+ message: string;
117
+ data: WholesaleRequestQueueEntry[];
118
+ LastEvaluatedKey?: string;
119
+ truncated?: boolean;
120
+ }
121
+ /**
122
+ * App `POST /customers` body that decides a request. `'approved'` requires a
123
+ * wholesale-class `priceList` and refuses a `reason`; `'declined'` refuses a
124
+ * `priceList`. `createdAt` is the request's own, as the queue served it: a
125
+ * request declined and resubmitted since is a different request, refused
126
+ * `409 WHOLESALE_REQUEST_NOT_PENDING` rather than decided unseen.
127
+ */
128
+ type WholesaleDecisionInput = {
129
+ mode: 'wholesale-decision';
130
+ customerId: string;
131
+ createdAt: number;
132
+ decision: 'approved';
133
+ priceList: number;
134
+ } | {
135
+ mode: 'wholesale-decision';
136
+ customerId: string;
137
+ createdAt: number;
138
+ decision: 'declined';
139
+ reason?: string;
140
+ };
141
+ /**
142
+ * App `POST /customers { mode: 'wholesale-decision' }` answer: the request as
143
+ * it now stands. `fiscalIdentityApplied` rides an APPROVAL only — `true` when
144
+ * the submitted CUIT, razón social and condición fiscal were written into
145
+ * `Customer.afip`; `false` when they were not, because the customer already
146
+ * held an `afip` entry or a top-level `cuit` naming another taxpayer (then
147
+ * neither `afip` nor `cuit` is written, and the list still moves).
148
+ */
149
+ interface WholesaleDecisionResponse {
150
+ message: string;
151
+ data: WholesaleRequest;
152
+ fiscalIdentityApplied?: boolean;
153
+ }
154
+ /**
155
+ * The `customers` frame a buyer's own socket receives when their request is
156
+ * decided. Carries no `priceList`: the CUSTOMER row's own `customers` frame
157
+ * carries that on approval.
158
+ */
159
+ interface WholesaleRequestStatusFrame {
160
+ customerId: string;
161
+ wholesaleRequest: WholesaleRequestStatus;
162
+ }
163
+ /**
164
+ * Which request a `CUSTOMER_WHOLESALE_REQUEST` operator bell is about. A
165
+ * POINTER, never an audience — the top-level `customerId` would deliver the
166
+ * bell to the buyer. Operator surfaces only.
167
+ */
168
+ interface WholesaleRequestRef {
169
+ customerId: string;
170
+ }
171
+ /**
172
+ * The request as it appears in the customer's own data export (both
173
+ * surfaces): the company data they submitted and the state, never the
174
+ * decline reason or the deciding operator.
175
+ */
176
+ interface WholesaleRequestExportEntry extends WholesaleRequestCompany {
177
+ /** The id the request was filed under — the subject's, or a merged-away id folded into it. */
178
+ customerId: string;
179
+ status: WholesaleRequestState;
180
+ createdAt: number;
181
+ decidedAt?: number;
182
+ }
183
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A signed-in storefront buyer's request for a wholesale account, and the
3
+ * operator queue that decides it.
4
+ *
5
+ * One row per customer (`WHOLESALEREQ#{storeId}` / `{customerId}`). A new
6
+ * request overwrites the previous one only once that one is decided, so a
7
+ * customer holds at most one pending request.
8
+ *
9
+ * The flow is offered only while the store's
10
+ * `ecommerce.config.wholesaleRequests` is `true` (absent = off).
11
+ */
12
+ /** The three states a request moves through. */
13
+ export const WHOLESALE_REQUEST_STATES = ['pending', 'approved', 'declined'];
14
+ /** `GET /customers?mode=wholesale-requests` page ceiling; the default page is 50. */
15
+ export const WHOLESALE_REQUESTS_PAGE_MAX = 100;
16
+ /** Character ceilings on the buyer's free text, counted after trimming. */
17
+ export const WHOLESALE_REQUEST_RAZON_SOCIAL_MAX = 200;
18
+ export const WHOLESALE_REQUEST_ACTIVITY_MAX = 120;
19
+ export const WHOLESALE_REQUEST_MESSAGE_MAX = 1000;
20
+ export const WHOLESALE_REQUEST_PHONE_MAX = 30;
21
+ /** Character ceiling on the operator's decline reason, counted after trimming. */
22
+ export const WHOLESALE_REQUEST_REASON_MAX = 500;
23
+ /**
24
+ * Every `error` code the wholesale-request flow answers on its own behalf:
25
+ * - web `POST /user { mode: 'wholesale-request' }` (`stacks/web/lambdas/user/_wholesaleRequest.ts`):
26
+ * `WHOLESALE_REQUESTS_DISABLED`, `CUSTOMER_NOT_FOUND`, `CUSTOMER_ANONYMIZED`, `USER_NOT_ENABLED`,
27
+ * `CUSTOMER_ALREADY_WHOLESALE`, `WHOLESALE_REQUEST_CONTENDED`, `WHOLESALE_REQUEST_PENDING`;
28
+ * - app `GET /customers?mode=wholesale-requests` and `POST /customers { mode: 'wholesale-decision' }`
29
+ * (`stacks/lambdas/customers/_wholesaleRequests.ts`): `ROLE_FORBIDDEN`, `ADMIN_REQUIRED`,
30
+ * `INVALID_CURSOR`, `WHOLESALE_REQUEST_NOT_PENDING`, `WHOLESALE_DECISION_CONTENDED`,
31
+ * `CUSTOMER_NOT_FOUND`, `CUSTOMER_ANONYMIZED`, `CUSTOMER_MERGED`, `PRICE_LIST_NOT_FOUND`,
32
+ * `PRICE_LIST_NOT_WHOLESALE`, `WHOLESALE_REQUEST_NOT_FOUND`;
33
+ * - the opt-in refusal both configuration writers answer when `ecommerce.config.wholesaleRequests`
34
+ * turns on with no wholesale-class list: `WHOLESALE_LIST_REQUIRED` (`409`). It is also in
35
+ * `STORE_PATCH_ERROR_CODES`, on purpose: a wholesale client handles it without reading that tuple.
36
+ *
37
+ * The rule: a code is listed when a wholesale handler writes it as its own literal, and each of
38
+ * those literals is typed `satisfies WholesaleRequestErrorCode`, so one this list misses fails
39
+ * typecheck. A code those routes forward from a shared helper is not listed, though a wholesale
40
+ * route can answer it: the customer-access refusals (`refuseCustomerAccess`), `VALIDATION_FAILED`
41
+ * (`validateRequest`), the handler wrapper's own codes and the authorizer's.
42
+ */
43
+ export const WHOLESALE_REQUEST_ERROR_CODES = [
44
+ 'WHOLESALE_REQUESTS_DISABLED',
45
+ 'CUSTOMER_NOT_FOUND',
46
+ 'CUSTOMER_ANONYMIZED',
47
+ 'USER_NOT_ENABLED',
48
+ 'CUSTOMER_ALREADY_WHOLESALE',
49
+ 'WHOLESALE_REQUEST_CONTENDED',
50
+ 'WHOLESALE_REQUEST_PENDING',
51
+ 'ROLE_FORBIDDEN',
52
+ 'ADMIN_REQUIRED',
53
+ 'INVALID_CURSOR',
54
+ 'WHOLESALE_REQUEST_NOT_PENDING',
55
+ 'WHOLESALE_DECISION_CONTENDED',
56
+ 'CUSTOMER_MERGED',
57
+ 'PRICE_LIST_NOT_FOUND',
58
+ 'PRICE_LIST_NOT_WHOLESALE',
59
+ 'WHOLESALE_REQUEST_NOT_FOUND',
60
+ 'WHOLESALE_LIST_REQUIRED',
61
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platform-types",
3
- "version": "1.10.777",
3
+ "version": "1.10.779",
4
4
  "description": "Shared wire and entity contracts for the platform.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",