platform-types 1.10.836 → 1.10.838

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CUSTOMER_DISCOUNT_MAX = exports.CUSTOMER_ACQUISITION_CHANNELS = exports.CUSTOMER_LIFECYCLE_STAGES = exports.CUSTOMER_KINDS = void 0;
3
+ exports.isWhatsappNoticesOptedIn = exports.CUSTOMER_DISCOUNT_MAX = exports.CUSTOMER_ACQUISITION_CHANNELS = exports.CUSTOMER_LIFECYCLE_STAGES = exports.CUSTOMER_KINDS = void 0;
4
4
  exports.CUSTOMER_KINDS = ['person', 'company'];
5
5
  exports.CUSTOMER_LIFECYCLE_STAGES = ['lead', 'active', 'inactive'];
6
6
  exports.CUSTOMER_ACQUISITION_CHANNELS = ['storefront', 'pos', 'marketplace', 'import', 'manual'];
@@ -11,3 +11,6 @@ exports.CUSTOMER_ACQUISITION_CHANNELS = ['storefront', 'pos', 'marketplace', 'im
11
11
  * `discount` unchanged (or omits it) still saves.
12
12
  */
13
13
  exports.CUSTOMER_DISCOUNT_MAX = 50;
14
+ /** Whether `notices` records a standing transactional WhatsApp opt-in: `optIn` present and `optOut` absent. */
15
+ const isWhatsappNoticesOptedIn = (notices) => notices?.optIn !== undefined && notices.optOut === undefined;
16
+ exports.isWhatsappNoticesOptedIn = isWhatsappNoticesOptedIn;
@@ -1,13 +1,16 @@
1
1
  "use strict";
2
2
  /**
3
- * Guest order status link — the read-only page a buyer with no account opens
4
- * from the order email (`/pedido/{token}`), and the route that mails a fresh
5
- * link to the address stored on the order.
3
+ * Guest order status link — the page a buyer with no account opens from the
4
+ * order email (`/pedido/{token}`), the one consent it can give there, and the
5
+ * route that mails a fresh link to the address stored on the order.
6
6
  *
7
- * The token (`gs1.…`) is a bearer credential over exactly ONE order, read-only:
8
- * it authorises no cart write, payment, cancellation, withdrawal or export. It
9
- * carries no expiry; the read is refused once the order was delivered (or
10
- * cancelled) more than `guestStatusLinkDaysAfterDelivery` days ago — unless a
7
+ * The token (`gs1.…`) is a bearer credential over exactly ONE order. It
8
+ * authorises the read and exactly ONE write: the transactional WhatsApp consent
9
+ * grant for the order's own customer in that store
10
+ * (`POST /orders/status/whatsapp-opt-in`, {@link GuestOrderWhatsappOptInRequest}).
11
+ * Nothing else — no cart write, payment, cancellation, withdrawal or export.
12
+ * Both routes authorise it identically. It carries no expiry; it is refused
13
+ * once the order was delivered (or cancelled) more than `guestStatusLinkDaysAfterDelivery` days ago — unless a
11
14
  * purchase withdrawal on it is not final or a refund on it is not closed — and
12
15
  * every earlier link dies when a recovery or an operator revokes it.
13
16
  *
@@ -16,7 +19,7 @@
16
19
  * code mailed to the order's address, so the token still authorises none.
17
20
  */
18
21
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.GUEST_ORDER_STATUS_LINK_TEMPLATE = exports.GUEST_ORDER_STATUS_ERROR_CODES = void 0;
22
+ exports.GUEST_ORDER_STATUS_LINK_TEMPLATE = exports.GUEST_ORDER_WHATSAPP_OPT_IN_ERROR_CODES = exports.GUEST_ORDER_STATUS_ERROR_CODES = void 0;
20
23
  /**
21
24
  * The refusals the guest status routes answer themselves.
22
25
  *
@@ -28,5 +31,15 @@ exports.GUEST_ORDER_STATUS_LINK_TEMPLATE = exports.GUEST_ORDER_STATUS_ERROR_CODE
28
31
  * `POST /orders/status/recover`.
29
32
  */
30
33
  exports.GUEST_ORDER_STATUS_ERROR_CODES = ['GUEST_STATUS_TOKEN_INVALID'];
34
+ /**
35
+ * The refusals `POST /orders/status/whatsapp-opt-in` answers itself, past the
36
+ * token (which refuses `GUEST_STATUS_TOKEN_INVALID` exactly as the read does).
37
+ *
38
+ * - WHATSAPP_OPT_IN_UNAVAILABLE (409): the offer does not hold for this order —
39
+ * the store's messaging is unavailable, the order is cancelled, delivered or
40
+ * disabled, carries no phone, or its customer row is no longer live. Nothing
41
+ * is written. One code for every reason.
42
+ */
43
+ exports.GUEST_ORDER_WHATSAPP_OPT_IN_ERROR_CODES = ['WHATSAPP_OPT_IN_UNAVAILABLE'];
31
44
  /** The transactional email template that mails a guest their (new) status link. */
32
45
  exports.GUEST_ORDER_STATUS_LINK_TEMPLATE = 'guest-order-status-link';
package/dist/cjs/store.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CHANGE_OF_MIND_WINDOW_SOURCE = exports.CHANGE_OF_MIND_RETURN_DAYS_MAX = exports.CHANGE_OF_MIND_REFUND_AS = exports.CHANGE_OF_MIND_RETURN_SHIPPING_PAYERS = exports.STORE_PATCH_ERROR_CODES = exports.STORE_MODULE_ERROR_CODES = exports.isStoreModuleOn = exports.isStoreModuleKey = exports.STORE_MODULE_KEYS = exports.STOREFRONT_METHOD_KINDS = exports.STORE_ONBOARDING_REVIEW_AREAS = exports.CUSTOM_DOMAIN_ROW_LIMIT = exports.CUSTOM_DOMAIN_SLOT_LIMIT = void 0;
3
+ exports.resolveStoreMessaging = exports.STORE_MESSAGING_SIGNATURE_MAX = exports.STORE_MESSAGING_DEFAULTS = exports.STORE_MESSAGE_NOTICES = exports.CHANGE_OF_MIND_WINDOW_SOURCE = exports.CHANGE_OF_MIND_RETURN_DAYS_MAX = exports.CHANGE_OF_MIND_REFUND_AS = exports.CHANGE_OF_MIND_RETURN_SHIPPING_PAYERS = exports.STORE_PATCH_ERROR_CODES = exports.STORE_MODULE_ERROR_CODES = exports.isStoreModuleOn = exports.isStoreModuleKey = exports.STORE_MODULE_KEYS = exports.STOREFRONT_METHOD_KINDS = exports.STORE_ONBOARDING_REVIEW_AREAS = exports.CUSTOM_DOMAIN_ROW_LIMIT = exports.CUSTOM_DOMAIN_SLOT_LIMIT = void 0;
4
4
  /**
5
5
  * How many custom-domain rows a store may hold that count: one active domain
6
6
  * and one spare, so a replacement can be verified and provisioned before the
@@ -119,6 +119,7 @@ exports.STORE_MODULE_ERROR_CODES = [
119
119
  * - `AFIP_RELACION_FLAG_FORBIDDEN` — `afip.wscdcEnabled` / `afip.fceEnabled`.
120
120
  * - `FX_TABLE_FORBIDDEN` — the `currencies` exchange-rate table.
121
121
  * - `FX_AUTO_UPDATE_FORBIDDEN` — `fxAutoUpdate`.
122
+ * - `STORE_MESSAGING_FORBIDDEN` — the `messaging` customer-messaging settings.
122
123
  *
123
124
  * Request refusals, `400`:
124
125
  * - `FIELD_NOT_WRITABLE_HERE` — a field written through `POST /store`, not
@@ -197,6 +198,7 @@ exports.STORE_PATCH_ERROR_CODES = [
197
198
  'NET_PLUS_IVA_REQUIRES_RI',
198
199
  'DATA_FISCAL_URL_INVALID',
199
200
  'STORE_PUBLIC_HOST_MISSING',
201
+ 'STORE_MESSAGING_FORBIDDEN',
200
202
  'FORBIDDEN',
201
203
  'STORE_NOT_FOUND',
202
204
  ];
@@ -208,3 +210,63 @@ exports.CHANGE_OF_MIND_REFUND_AS = ['refund', 'store-credit'];
208
210
  exports.CHANGE_OF_MIND_RETURN_DAYS_MAX = 365;
209
211
  /** The source of a change-of-mind deadline: the store's own setting, not a statutory row. */
210
212
  exports.CHANGE_OF_MIND_WINDOW_SOURCE = 'store:changeOfMindReturns';
213
+ /**
214
+ * Every `StoreMessageNotice`, in the order the settings screen lists them —
215
+ * the keys `Store.integrations.messaging` switches. `UncoveredStoreMessageNotices`
216
+ * fails `typecheck` naming a notice the union gains and this tuple lacks.
217
+ */
218
+ exports.STORE_MESSAGE_NOTICES = [
219
+ 'orderReady',
220
+ 'orderShipped',
221
+ 'serviceOrderReceived',
222
+ 'serviceOrderQuoted',
223
+ 'serviceOrderApproved',
224
+ 'serviceOrderReady',
225
+ 'serviceOrderCancelled',
226
+ 'serviceOrderReturnedUnrepaired',
227
+ 'serviceOrderAbandoned',
228
+ 'serviceOrderOnHold',
229
+ 'serviceOrderResumed',
230
+ 'appointmentReminder',
231
+ 'paymentReminder',
232
+ ];
233
+ /**
234
+ * What an ABSENT `messaging` key reads: status notices and appointment
235
+ * reminders on, payment reminders off, the SMS fallback on.
236
+ */
237
+ exports.STORE_MESSAGING_DEFAULTS = {
238
+ orderReady: true,
239
+ orderShipped: true,
240
+ serviceOrderReceived: true,
241
+ serviceOrderQuoted: true,
242
+ serviceOrderApproved: true,
243
+ serviceOrderReady: true,
244
+ serviceOrderCancelled: true,
245
+ serviceOrderReturnedUnrepaired: true,
246
+ serviceOrderAbandoned: true,
247
+ serviceOrderOnHold: true,
248
+ serviceOrderResumed: true,
249
+ appointmentReminder: true,
250
+ paymentReminder: false,
251
+ smsFallback: true,
252
+ };
253
+ /** The firma cap — the same one `sms.signature` has. */
254
+ exports.STORE_MESSAGING_SIGNATURE_MAX = 80;
255
+ /**
256
+ * Every switch of a store's messaging settings, decided: per key, the stored
257
+ * boolean, else its `STORE_MESSAGING_DEFAULTS` value (a stored non-boolean
258
+ * reads as absent). `signature` falls back to `sms.signature` only when
259
+ * `messaging.signature` is absent; a stored `''` is kept and means no firma.
260
+ */
261
+ const resolveStoreMessaging = (integrations) => {
262
+ const leaf = integrations?.messaging;
263
+ const resolved = { ...exports.STORE_MESSAGING_DEFAULTS };
264
+ for (const key of [...exports.STORE_MESSAGE_NOTICES, 'smsFallback']) {
265
+ const stored = leaf?.[key];
266
+ if (typeof stored === 'boolean')
267
+ resolved[key] = stored;
268
+ }
269
+ const signature = typeof leaf?.signature === 'string' ? leaf.signature : integrations?.sms?.signature;
270
+ return typeof signature === 'string' ? { ...resolved, signature } : resolved;
271
+ };
272
+ exports.resolveStoreMessaging = resolveStoreMessaging;
@@ -132,4 +132,5 @@ exports.FEATURE_KEYS = [
132
132
  'webhooks',
133
133
  'maxOrdersMonth',
134
134
  'maxInvoicesMonth',
135
+ 'monthlyMessageAllowance',
135
136
  ];
@@ -8,6 +8,8 @@ export declare const CUSTOMER_ACQUISITION_CHANNELS: readonly ["storefront", "pos
8
8
  * `discount` unchanged (or omits it) still saves.
9
9
  */
10
10
  export declare const CUSTOMER_DISCOUNT_MAX = 50;
11
+ /** Whether `notices` records a standing transactional WhatsApp opt-in: `optIn` present and `optOut` absent. */
12
+ export declare const isWhatsappNoticesOptedIn: (notices: CustomerWhatsappNotices | undefined) => boolean;
11
13
  declare global {
12
14
  /** Exact identity matches within the token's store; no stored identity fields are returned. */
13
15
  interface CustomerDuplicateCandidate {
@@ -95,8 +97,62 @@ declare global {
95
97
  /** Per-customer, not per-channel — one unsubscribe link covers every channel. */
96
98
  unsubscribeToken?: string;
97
99
  }
98
- /** Write shape for customer create/update — carries the transient photo controls. */
99
- type CustomerUpsertInput = Partial<Customer> & PhotoUploadControls;
100
+ /**
101
+ * Where a transactional WhatsApp grant or withdrawal was given:
102
+ * `back-office` (staff, `POST /customers`), `storefront-post-purchase` (the
103
+ * one-tap offer after an order is placed — the signed-in banner through web
104
+ * `POST /user` with `whatsappOptInOrigin: 'post-purchase'`, or a guest's
105
+ * `POST /orders/status/whatsapp-opt-in`) and `storefront-profile` (the
106
+ * shopper's own profile toggle, web `POST /user`).
107
+ */
108
+ type WhatsappNoticeSource = 'back-office' | 'storefront-post-purchase' | 'storefront-profile';
109
+ /**
110
+ * Provenance of one transactional WhatsApp grant or withdrawal.
111
+ * Server-written only. Carries NO phone number and NO IP, unlike
112
+ * `ChannelConsentStamp` (deliberately not extended), and not who acted.
113
+ */
114
+ interface WhatsappNoticeStamp {
115
+ /** ms epoch. */
116
+ ts: number;
117
+ source: WhatsappNoticeSource;
118
+ }
119
+ /**
120
+ * `Customer.whatsappNotices`: the customer's consent to the store's
121
+ * transactional WhatsApp notices (order and service-order status,
122
+ * reminders). Per customer per store, since the CUSTOMER row is
123
+ * store-scoped. Separate from `CustomerMarketing`, and never marketing
124
+ * consent.
125
+ *
126
+ * Opted in ⇔ `optIn` present AND `optOut` absent; read it through
127
+ * `isWhatsappNoticesOptedIn`. A grant SETs `optIn` and REMOVEs `optOut`; a
128
+ * withdrawal SETs `optOut` and keeps `optIn`. Resending the current state
129
+ * changes nothing and does not re-stamp; a withdrawal from a customer who
130
+ * never opted in is recorded. Not a history: it holds the latest grant and
131
+ * the latest withdrawal only. ⚠️ Absent is NOT consent.
132
+ */
133
+ interface CustomerWhatsappNotices {
134
+ optIn?: WhatsappNoticeStamp;
135
+ optOut?: WhatsappNoticeStamp;
136
+ }
137
+ /** Write shape: `whatsappNotices` is server-owned, and `whatsappOptIn` is the directive that moves it. */
138
+ type CustomerUpsertInput = Partial<Omit<Customer, 'whatsappNotices'>> & PhotoUploadControls & {
139
+ /**
140
+ * `POST /customers` (source `back-office`) and web `POST /user` (source
141
+ * `storefront-profile`): `true` grants, `false` withdraws, absent changes
142
+ * nothing. Never stored. Staff MAY grant, because this is not marketing
143
+ * consent. A storefront grant acts only while `Store.messagingAvailable`
144
+ * holds and is otherwise dropped; a withdrawal always acts.
145
+ */
146
+ whatsappOptIn?: boolean;
147
+ /**
148
+ * Web `POST /user` only: `'post-purchase'` names the grant as the one-tap
149
+ * offer after an order (`whatsappOptInOffer` on the order read), so the
150
+ * stamp's source is `storefront-post-purchase` instead of
151
+ * `storefront-profile`. A directive, never stored, and ignored unless
152
+ * `whatsappOptIn` is `true`.
153
+ */
154
+ whatsappOptInOrigin?: 'post-purchase';
155
+ };
100
156
  /**
101
157
  * The query string of the customers LIST read — `GET /customers` with no
102
158
  * `customerId`, `list` or `email`. Every present filter AND-composes: a row is
@@ -568,6 +624,8 @@ declare global {
568
624
  */
569
625
  mergeBalanceCarried?: boolean;
570
626
  marketing?: CustomerMarketing;
627
+ /** Transactional WhatsApp consent for this store — see `CustomerWhatsappNotices`. Server-written. */
628
+ whatsappNotices?: CustomerWhatsappNotices;
571
629
  /** Optional notification mail only; transactional mail cannot be disabled. */
572
630
  notificationPreferences?: CustomerNotificationPreferences;
573
631
  minBuy?: number;
package/dist/customer.js CHANGED
@@ -8,3 +8,5 @@ export const CUSTOMER_ACQUISITION_CHANNELS = ['storefront', 'pos', 'marketplace'
8
8
  * `discount` unchanged (or omits it) still saves.
9
9
  */
10
10
  export const CUSTOMER_DISCOUNT_MAX = 50;
11
+ /** Whether `notices` records a standing transactional WhatsApp opt-in: `optIn` present and `optOut` absent. */
12
+ export const isWhatsappNoticesOptedIn = (notices) => notices?.optIn !== undefined && notices.optOut === undefined;
@@ -1,12 +1,15 @@
1
1
  /**
2
- * Guest order status link — the read-only page a buyer with no account opens
3
- * from the order email (`/pedido/{token}`), and the route that mails a fresh
4
- * link to the address stored on the order.
2
+ * Guest order status link — the page a buyer with no account opens from the
3
+ * order email (`/pedido/{token}`), the one consent it can give there, and the
4
+ * route that mails a fresh link to the address stored on the order.
5
5
  *
6
- * The token (`gs1.…`) is a bearer credential over exactly ONE order, read-only:
7
- * it authorises no cart write, payment, cancellation, withdrawal or export. It
8
- * carries no expiry; the read is refused once the order was delivered (or
9
- * cancelled) more than `guestStatusLinkDaysAfterDelivery` days ago — unless a
6
+ * The token (`gs1.…`) is a bearer credential over exactly ONE order. It
7
+ * authorises the read and exactly ONE write: the transactional WhatsApp consent
8
+ * grant for the order's own customer in that store
9
+ * (`POST /orders/status/whatsapp-opt-in`, {@link GuestOrderWhatsappOptInRequest}).
10
+ * Nothing else — no cart write, payment, cancellation, withdrawal or export.
11
+ * Both routes authorise it identically. It carries no expiry; it is refused
12
+ * once the order was delivered (or cancelled) more than `guestStatusLinkDaysAfterDelivery` days ago — unless a
10
13
  * purchase withdrawal on it is not final or a refund on it is not closed — and
11
14
  * every earlier link dies when a recovery or an operator revokes it.
12
15
  *
@@ -26,6 +29,17 @@
26
29
  */
27
30
  export declare const GUEST_ORDER_STATUS_ERROR_CODES: readonly ["GUEST_STATUS_TOKEN_INVALID"];
28
31
  export type GuestOrderStatusErrorCode = (typeof GUEST_ORDER_STATUS_ERROR_CODES)[number];
32
+ /**
33
+ * The refusals `POST /orders/status/whatsapp-opt-in` answers itself, past the
34
+ * token (which refuses `GUEST_STATUS_TOKEN_INVALID` exactly as the read does).
35
+ *
36
+ * - WHATSAPP_OPT_IN_UNAVAILABLE (409): the offer does not hold for this order —
37
+ * the store's messaging is unavailable, the order is cancelled, delivered or
38
+ * disabled, carries no phone, or its customer row is no longer live. Nothing
39
+ * is written. One code for every reason.
40
+ */
41
+ export declare const GUEST_ORDER_WHATSAPP_OPT_IN_ERROR_CODES: readonly ["WHATSAPP_OPT_IN_UNAVAILABLE"];
42
+ export type GuestOrderWhatsappOptInErrorCode = (typeof GUEST_ORDER_WHATSAPP_OPT_IN_ERROR_CODES)[number];
29
43
  /** The transactional email template that mails a guest their (new) status link. */
30
44
  export declare const GUEST_ORDER_STATUS_LINK_TEMPLATE: "guest-order-status-link";
31
45
  declare global {
@@ -115,6 +129,17 @@ declare global {
115
129
  returnRequests?: OrderReturnRequestView[];
116
130
  /** Whether a return request can be opened now. Absent only when the return requests could not be read whole. */
117
131
  returnRequestEligibility?: OrderReturnRequestEligibility;
132
+ /**
133
+ * Present, and `true`, only when the post-purchase «Avisarme por WhatsApp
134
+ * sobre este pedido» may be offered: the store's messaging is available
135
+ * (`Store.messagingAvailable`), the order is not cancelled, delivered or
136
+ * disabled, it carries a phone (its customer's or the guest contact's),
137
+ * and its customer row is live and NOT opted in
138
+ * (`Customer.whatsappNotices`). Accept it with
139
+ * `POST /orders/status/whatsapp-opt-in`. Carries no phone. Absent too when
140
+ * the offer could not be computed — the read never fails for it.
141
+ */
142
+ whatsappOptInOffer?: true;
118
143
  contact: {
119
144
  /** The stored address, masked (e.g. `j***@d***.com`). */
120
145
  emailMasked: string;
@@ -131,4 +156,21 @@ declare global {
131
156
  orderId: string;
132
157
  email: string;
133
158
  }
159
+ /**
160
+ * `POST /orders/status/whatsapp-opt-in` body (strict): the guest status token
161
+ * of the order whose post-purchase WhatsApp offer (`whatsappOptInOffer`) is
162
+ * accepted. The token is authorised exactly as `GET /orders/status` does it
163
+ * (one `401 GUEST_STATUS_TOKEN_INVALID` for every refusal; a malformed body
164
+ * too). Then the grant: `200` {@link GuestOrderWhatsappOptInResult} when the
165
+ * customer is now opted in — freshly stamped `{ ts, source:
166
+ * 'storefront-post-purchase' }`, or already opted in (nothing written, never
167
+ * re-stamped) — else `409 WHATSAPP_OPT_IN_UNAVAILABLE`, nothing written.
168
+ */
169
+ interface GuestOrderWhatsappOptInRequest {
170
+ token: string;
171
+ }
172
+ /** `data` of a `200` `POST /orders/status/whatsapp-opt-in`. */
173
+ interface GuestOrderWhatsappOptInResult {
174
+ whatsappNoticesOptedIn: true;
175
+ }
134
176
  }
@@ -1,12 +1,15 @@
1
1
  /**
2
- * Guest order status link — the read-only page a buyer with no account opens
3
- * from the order email (`/pedido/{token}`), and the route that mails a fresh
4
- * link to the address stored on the order.
2
+ * Guest order status link — the page a buyer with no account opens from the
3
+ * order email (`/pedido/{token}`), the one consent it can give there, and the
4
+ * route that mails a fresh link to the address stored on the order.
5
5
  *
6
- * The token (`gs1.…`) is a bearer credential over exactly ONE order, read-only:
7
- * it authorises no cart write, payment, cancellation, withdrawal or export. It
8
- * carries no expiry; the read is refused once the order was delivered (or
9
- * cancelled) more than `guestStatusLinkDaysAfterDelivery` days ago — unless a
6
+ * The token (`gs1.…`) is a bearer credential over exactly ONE order. It
7
+ * authorises the read and exactly ONE write: the transactional WhatsApp consent
8
+ * grant for the order's own customer in that store
9
+ * (`POST /orders/status/whatsapp-opt-in`, {@link GuestOrderWhatsappOptInRequest}).
10
+ * Nothing else — no cart write, payment, cancellation, withdrawal or export.
11
+ * Both routes authorise it identically. It carries no expiry; it is refused
12
+ * once the order was delivered (or cancelled) more than `guestStatusLinkDaysAfterDelivery` days ago — unless a
10
13
  * purchase withdrawal on it is not final or a refund on it is not closed — and
11
14
  * every earlier link dies when a recovery or an operator revokes it.
12
15
  *
@@ -25,5 +28,15 @@
25
28
  * `POST /orders/status/recover`.
26
29
  */
27
30
  export const GUEST_ORDER_STATUS_ERROR_CODES = ['GUEST_STATUS_TOKEN_INVALID'];
31
+ /**
32
+ * The refusals `POST /orders/status/whatsapp-opt-in` answers itself, past the
33
+ * token (which refuses `GUEST_STATUS_TOKEN_INVALID` exactly as the read does).
34
+ *
35
+ * - WHATSAPP_OPT_IN_UNAVAILABLE (409): the offer does not hold for this order —
36
+ * the store's messaging is unavailable, the order is cancelled, delivered or
37
+ * disabled, carries no phone, or its customer row is no longer live. Nothing
38
+ * is written. One code for every reason.
39
+ */
40
+ export const GUEST_ORDER_WHATSAPP_OPT_IN_ERROR_CODES = ['WHATSAPP_OPT_IN_UNAVAILABLE'];
28
41
  /** The transactional email template that mails a guest their (new) status link. */
29
42
  export const GUEST_ORDER_STATUS_LINK_TEMPLATE = 'guest-order-status-link';
@@ -145,6 +145,18 @@ declare global {
145
145
  returnRequests?: OrderReturnRequestView[];
146
146
  /** Whether a return request can be opened now. Absent only when the return requests could not be read whole. */
147
147
  returnRequestEligibility?: OrderReturnRequestEligibility;
148
+ /**
149
+ * Point read (`?orderId=`) only, never the list. Present, and `true`, only
150
+ * when the post-purchase «Avisarme por WhatsApp sobre este pedido» may be
151
+ * offered: the store's messaging is available (`Store.messagingAvailable`),
152
+ * the order is not cancelled, delivered or disabled, it carries a phone
153
+ * (`customer.phone` or the guest contact's), and the order's customer row
154
+ * is live and NOT opted in (`Customer.whatsappNotices`). Accept it with web
155
+ * `POST /user` `{ whatsappOptIn: true, whatsappOptInOrigin: 'post-purchase' }`.
156
+ * Carries no phone. Absent too when the offer could not be computed — the
157
+ * order read never fails for it.
158
+ */
159
+ whatsappOptInOffer?: true;
148
160
  };
149
161
  /**
150
162
  * `data` of an `orderPaymentAttempt` socket frame, sent to the order's own
package/dist/return.d.ts CHANGED
@@ -771,6 +771,13 @@ declare global {
771
771
  cashRefund?: ReturnCashRefund;
772
772
  /** Mirrors Return.owedReduction: the unpaid part of a walk-in return, which lowered what the order owes. */
773
773
  owedReduction?: number;
774
+ /**
775
+ * Mirrors `Return.freight`, written in the same transaction: the freight line a
776
+ * `purchase-withdrawal` return gave back, already inside `total`. Shown to the shopper as well
777
+ * (the money is theirs). ⚠️ Optional permanently: absent on every return that keeps the freight and on
778
+ * every summary written before it existed — read the RETURN row for those.
779
+ */
780
+ freight?: ReturnFreight;
774
781
  reason: ReturnReason;
775
782
  /**
776
783
  * Mirrors `Return.flow`, written in the same transaction. Operator-only:
package/dist/store.d.ts CHANGED
@@ -298,7 +298,8 @@ declare global {
298
298
  * Platform switch for store-paid customer messaging (SMS today). `false`
299
299
  * or absent: no store-paid message is sent, and the app hides every store
300
300
  * messaging surface. App-only (`forwardToTenants: 'app'`), so absent from
301
- * the storefront's unauthenticated response.
301
+ * the storefront's unauthenticated response: the storefront reads
302
+ * `Store.messagingAvailable` instead, which folds this switch in.
302
303
  */
303
304
  tenantMessagingEnabled?: boolean;
304
305
  }
@@ -634,6 +635,16 @@ declare global {
634
635
  * first such removal. No request body may set it.
635
636
  */
636
637
  adminRemovalGeneration?: number;
638
+ /**
639
+ * Web `GET /store` only, always a boolean on a 200: `true` when the
640
+ * `tenantMessagingEnabled` platform global is on and at least one
641
+ * `StoreMessageNotice` resolves on (`resolveStoreMessaging`). Computed when
642
+ * the route is read, never stored or writable. Not on `stores` frames or the
643
+ * preview frame, where absent reads `false`. Checkout hides «Avisarme por
644
+ * WhatsApp» when it is `false`, and a storefront opt-in sent while it is
645
+ * `false` is dropped.
646
+ */
647
+ messagingAvailable?: boolean;
637
648
  storeId: string;
638
649
  createdAt: number;
639
650
  /** Unix ms — BE-stamped on every `POST`/`PATCH /store` write. Always present on written rows. */
@@ -1417,7 +1428,15 @@ declare global {
1417
1428
  interface StoreIntegrations {
1418
1429
  afip?: Afip;
1419
1430
  mercadopago?: Mercadopago;
1431
+ /**
1432
+ * Per-store SMS entitlement and firma. Kept READABLE for migration: the
1433
+ * app writes `messaging` instead, and `PATCH /store` still accepts `sms`
1434
+ * for builds that have not moved. `messaging` never reads `sms.enabled`;
1435
+ * it falls back to `sms.signature` only while its own `signature` is absent.
1436
+ */
1420
1437
  sms?: SmsIntegration;
1438
+ /** The store's customer-messaging settings — see `StoreMessagingIntegration`. */
1439
+ messaging?: StoreMessagingIntegration;
1421
1440
  gmail?: Gmail;
1422
1441
  mercadolibre?: Mercadolibre;
1423
1442
  tiendanube?: Tiendanube;
@@ -1427,6 +1446,37 @@ declare global {
1427
1446
  enabled?: boolean;
1428
1447
  signature?: string;
1429
1448
  }
1449
+ /**
1450
+ * One store-paid customer message a store switches on or off — one of
1451
+ * `STORE_MESSAGE_NOTICES`. Each maps to exactly one notice the api emits.
1452
+ * `orderShipped` covers dispatch (shipped and delivered); `serviceOrderQuoted`
1453
+ * covers the quote-sent notice and the `quoted` status fallback alike.
1454
+ */
1455
+ type StoreMessageNotice = 'orderReady' | 'orderShipped' | 'serviceOrderReceived' | 'serviceOrderQuoted' | 'serviceOrderApproved' | 'serviceOrderReady' | 'serviceOrderCancelled' | 'serviceOrderReturnedUnrepaired' | 'serviceOrderAbandoned' | 'serviceOrderOnHold' | 'serviceOrderResumed' | 'appointmentReminder' | 'paymentReminder';
1456
+ /**
1457
+ * `Store.integrations.messaging`. Written by `PATCH /store` only (ADMIN,
1458
+ * SUPERVISOR, MANAGER; a PRINTER claim is refused), merged key by key, so a
1459
+ * body naming one switch leaves the others as stored. An absent key reads
1460
+ * its `STORE_MESSAGING_DEFAULTS` value: status notices and appointment
1461
+ * reminders on, payment reminders off, `smsFallback` on. Read it through
1462
+ * `resolveStoreMessaging`, never off the raw keys. Sending is gated by the
1463
+ * `tenantMessagingEnabled` platform global as well.
1464
+ */
1465
+ interface StoreMessagingIntegration extends Partial<Record<StoreMessageNotice, boolean>> {
1466
+ /** «Si WhatsApp falla, enviar por SMS». The SMS goes out only on a WhatsApp `failed`/`undelivered` status. */
1467
+ smsFallback?: boolean;
1468
+ /**
1469
+ * Firma, at most 80 characters after trimming (the cap `sms.signature`
1470
+ * has). Absent falls back to `sms.signature`; `''` means no firma.
1471
+ */
1472
+ signature?: string;
1473
+ }
1474
+ /** `resolveStoreMessaging`'s answer: every switch decided. */
1475
+ interface ResolvedStoreMessaging extends Record<StoreMessageNotice, boolean> {
1476
+ smsFallback: boolean;
1477
+ /** Absent when neither `messaging.signature` nor `sms.signature` is stored. */
1478
+ signature?: string;
1479
+ }
1430
1480
  interface Gmail {
1431
1481
  connected?: boolean;
1432
1482
  senderEmail?: string;
@@ -1962,6 +2012,13 @@ declare global {
1962
2012
  * still broadcasts the row as it stands.
1963
2013
  */
1964
2014
  onboardingReviewAdd?: StoreOnboardingReviewArea[];
2015
+ /**
2016
+ * `PATCH /store` only (the closed `POST /store` refuses it
2017
+ * `FIELD_NOT_WRITABLE_HERE`). Merged key by key over the stored leaf; an
2018
+ * undeclared key is a 400. A plain USER is refused 403
2019
+ * `STORE_MESSAGING_FORBIDDEN`.
2020
+ */
2021
+ messaging?: StoreMessagingIntegration;
1965
2022
  }
1966
2023
  /**
1967
2024
  * How the storefront offers a `paymentMethods` row — one of
@@ -2639,6 +2696,7 @@ export type StoreModuleRefusalBody = {
2639
2696
  * - `AFIP_RELACION_FLAG_FORBIDDEN` — `afip.wscdcEnabled` / `afip.fceEnabled`.
2640
2697
  * - `FX_TABLE_FORBIDDEN` — the `currencies` exchange-rate table.
2641
2698
  * - `FX_AUTO_UPDATE_FORBIDDEN` — `fxAutoUpdate`.
2699
+ * - `STORE_MESSAGING_FORBIDDEN` — the `messaging` customer-messaging settings.
2642
2700
  *
2643
2701
  * Request refusals, `400`:
2644
2702
  * - `FIELD_NOT_WRITABLE_HERE` — a field written through `POST /store`, not
@@ -2694,7 +2752,7 @@ export type StoreModuleRefusalBody = {
2694
2752
  * checks, the display-currency lock, the storefront fence) are not listed
2695
2753
  * either. Cross-cutting refusals: check `GATING_ERROR_CODES` as well.
2696
2754
  */
2697
- export declare const STORE_PATCH_ERROR_CODES: readonly ["STORE_CONFIG_FORBIDDEN", "STORE_ECOMMERCE_FORBIDDEN", "STORE_FISCAL_FIELD_FORBIDDEN", "STORE_JURISDICTION_FORBIDDEN", "AFIP_RELACION_FLAG_FORBIDDEN", "FX_TABLE_FORBIDDEN", "FX_AUTO_UPDATE_FORBIDDEN", "FIELD_NOT_WRITABLE_HERE", "AFIP_POINT_OF_SALE_NOT_CLEARABLE", "CURRENCY_CATALOG_ID_UNKNOWN", "ARS_RATE_MUST_BE_ONE", "CURRENCY_RATE_ABOVE_CEILING", "FX_CATALOG_ID_UNKNOWN", "FX_SOURCE_ID_UNKNOWN", "DISPLAY_CURRENCY_NOT_INVOICEABLE", "PRODUCT_CURRENCY_NOT_INVOICEABLE", "PRICE_LIST_NOT_FOUND", "DELIVERY_FEE_INVALID", "WHOLESALE_LIST_REQUIRED", "NET_PLUS_IVA_REQUIRES_RI", "DATA_FISCAL_URL_INVALID", "STORE_PUBLIC_HOST_MISSING", "FORBIDDEN", "STORE_NOT_FOUND"];
2755
+ export declare const STORE_PATCH_ERROR_CODES: readonly ["STORE_CONFIG_FORBIDDEN", "STORE_ECOMMERCE_FORBIDDEN", "STORE_FISCAL_FIELD_FORBIDDEN", "STORE_JURISDICTION_FORBIDDEN", "AFIP_RELACION_FLAG_FORBIDDEN", "FX_TABLE_FORBIDDEN", "FX_AUTO_UPDATE_FORBIDDEN", "FIELD_NOT_WRITABLE_HERE", "AFIP_POINT_OF_SALE_NOT_CLEARABLE", "CURRENCY_CATALOG_ID_UNKNOWN", "ARS_RATE_MUST_BE_ONE", "CURRENCY_RATE_ABOVE_CEILING", "FX_CATALOG_ID_UNKNOWN", "FX_SOURCE_ID_UNKNOWN", "DISPLAY_CURRENCY_NOT_INVOICEABLE", "PRODUCT_CURRENCY_NOT_INVOICEABLE", "PRICE_LIST_NOT_FOUND", "DELIVERY_FEE_INVALID", "WHOLESALE_LIST_REQUIRED", "NET_PLUS_IVA_REQUIRES_RI", "DATA_FISCAL_URL_INVALID", "STORE_PUBLIC_HOST_MISSING", "STORE_MESSAGING_FORBIDDEN", "FORBIDDEN", "STORE_NOT_FOUND"];
2698
2756
  export type StorePatchErrorCode = (typeof STORE_PATCH_ERROR_CODES)[number];
2699
2757
  /** Who pays the return shipping under a store's change-of-mind policy. */
2700
2758
  export declare const CHANGE_OF_MIND_RETURN_SHIPPING_PAYERS: readonly ["merchant", "shopper"];
@@ -2716,3 +2774,41 @@ declare global {
2716
2774
  refundAs: ChangeOfMindRefundAs;
2717
2775
  }
2718
2776
  }
2777
+ /**
2778
+ * Every `StoreMessageNotice`, in the order the settings screen lists them —
2779
+ * the keys `Store.integrations.messaging` switches. `UncoveredStoreMessageNotices`
2780
+ * fails `typecheck` naming a notice the union gains and this tuple lacks.
2781
+ */
2782
+ export declare const STORE_MESSAGE_NOTICES: readonly ["orderReady", "orderShipped", "serviceOrderReceived", "serviceOrderQuoted", "serviceOrderApproved", "serviceOrderReady", "serviceOrderCancelled", "serviceOrderReturnedUnrepaired", "serviceOrderAbandoned", "serviceOrderOnHold", "serviceOrderResumed", "appointmentReminder", "paymentReminder"];
2783
+ type AssertNoUncoveredStoreMessageNotice<T extends never> = T;
2784
+ export type UncoveredStoreMessageNotices = AssertNoUncoveredStoreMessageNotice<Exclude<StoreMessageNotice, (typeof STORE_MESSAGE_NOTICES)[number]>>;
2785
+ /**
2786
+ * What an ABSENT `messaging` key reads: status notices and appointment
2787
+ * reminders on, payment reminders off, the SMS fallback on.
2788
+ */
2789
+ export declare const STORE_MESSAGING_DEFAULTS: {
2790
+ readonly orderReady: true;
2791
+ readonly orderShipped: true;
2792
+ readonly serviceOrderReceived: true;
2793
+ readonly serviceOrderQuoted: true;
2794
+ readonly serviceOrderApproved: true;
2795
+ readonly serviceOrderReady: true;
2796
+ readonly serviceOrderCancelled: true;
2797
+ readonly serviceOrderReturnedUnrepaired: true;
2798
+ readonly serviceOrderAbandoned: true;
2799
+ readonly serviceOrderOnHold: true;
2800
+ readonly serviceOrderResumed: true;
2801
+ readonly appointmentReminder: true;
2802
+ readonly paymentReminder: false;
2803
+ readonly smsFallback: true;
2804
+ };
2805
+ /** The firma cap — the same one `sms.signature` has. */
2806
+ export declare const STORE_MESSAGING_SIGNATURE_MAX = 80;
2807
+ /**
2808
+ * Every switch of a store's messaging settings, decided: per key, the stored
2809
+ * boolean, else its `STORE_MESSAGING_DEFAULTS` value (a stored non-boolean
2810
+ * reads as absent). `signature` falls back to `sms.signature` only when
2811
+ * `messaging.signature` is absent; a stored `''` is kept and means no firma.
2812
+ */
2813
+ export declare const resolveStoreMessaging: (integrations: Pick<StoreIntegrations, "messaging" | "sms"> | undefined) => ResolvedStoreMessaging;
2814
+ export {};
package/dist/store.js CHANGED
@@ -114,6 +114,7 @@ export const STORE_MODULE_ERROR_CODES = [
114
114
  * - `AFIP_RELACION_FLAG_FORBIDDEN` — `afip.wscdcEnabled` / `afip.fceEnabled`.
115
115
  * - `FX_TABLE_FORBIDDEN` — the `currencies` exchange-rate table.
116
116
  * - `FX_AUTO_UPDATE_FORBIDDEN` — `fxAutoUpdate`.
117
+ * - `STORE_MESSAGING_FORBIDDEN` — the `messaging` customer-messaging settings.
117
118
  *
118
119
  * Request refusals, `400`:
119
120
  * - `FIELD_NOT_WRITABLE_HERE` — a field written through `POST /store`, not
@@ -192,6 +193,7 @@ export const STORE_PATCH_ERROR_CODES = [
192
193
  'NET_PLUS_IVA_REQUIRES_RI',
193
194
  'DATA_FISCAL_URL_INVALID',
194
195
  'STORE_PUBLIC_HOST_MISSING',
196
+ 'STORE_MESSAGING_FORBIDDEN',
195
197
  'FORBIDDEN',
196
198
  'STORE_NOT_FOUND',
197
199
  ];
@@ -203,3 +205,62 @@ export const CHANGE_OF_MIND_REFUND_AS = ['refund', 'store-credit'];
203
205
  export const CHANGE_OF_MIND_RETURN_DAYS_MAX = 365;
204
206
  /** The source of a change-of-mind deadline: the store's own setting, not a statutory row. */
205
207
  export const CHANGE_OF_MIND_WINDOW_SOURCE = 'store:changeOfMindReturns';
208
+ /**
209
+ * Every `StoreMessageNotice`, in the order the settings screen lists them —
210
+ * the keys `Store.integrations.messaging` switches. `UncoveredStoreMessageNotices`
211
+ * fails `typecheck` naming a notice the union gains and this tuple lacks.
212
+ */
213
+ export const STORE_MESSAGE_NOTICES = [
214
+ 'orderReady',
215
+ 'orderShipped',
216
+ 'serviceOrderReceived',
217
+ 'serviceOrderQuoted',
218
+ 'serviceOrderApproved',
219
+ 'serviceOrderReady',
220
+ 'serviceOrderCancelled',
221
+ 'serviceOrderReturnedUnrepaired',
222
+ 'serviceOrderAbandoned',
223
+ 'serviceOrderOnHold',
224
+ 'serviceOrderResumed',
225
+ 'appointmentReminder',
226
+ 'paymentReminder',
227
+ ];
228
+ /**
229
+ * What an ABSENT `messaging` key reads: status notices and appointment
230
+ * reminders on, payment reminders off, the SMS fallback on.
231
+ */
232
+ export const STORE_MESSAGING_DEFAULTS = {
233
+ orderReady: true,
234
+ orderShipped: true,
235
+ serviceOrderReceived: true,
236
+ serviceOrderQuoted: true,
237
+ serviceOrderApproved: true,
238
+ serviceOrderReady: true,
239
+ serviceOrderCancelled: true,
240
+ serviceOrderReturnedUnrepaired: true,
241
+ serviceOrderAbandoned: true,
242
+ serviceOrderOnHold: true,
243
+ serviceOrderResumed: true,
244
+ appointmentReminder: true,
245
+ paymentReminder: false,
246
+ smsFallback: true,
247
+ };
248
+ /** The firma cap — the same one `sms.signature` has. */
249
+ export const STORE_MESSAGING_SIGNATURE_MAX = 80;
250
+ /**
251
+ * Every switch of a store's messaging settings, decided: per key, the stored
252
+ * boolean, else its `STORE_MESSAGING_DEFAULTS` value (a stored non-boolean
253
+ * reads as absent). `signature` falls back to `sms.signature` only when
254
+ * `messaging.signature` is absent; a stored `''` is kept and means no firma.
255
+ */
256
+ export const resolveStoreMessaging = (integrations) => {
257
+ const leaf = integrations?.messaging;
258
+ const resolved = { ...STORE_MESSAGING_DEFAULTS };
259
+ for (const key of [...STORE_MESSAGE_NOTICES, 'smsFallback']) {
260
+ const stored = leaf?.[key];
261
+ if (typeof stored === 'boolean')
262
+ resolved[key] = stored;
263
+ }
264
+ const signature = typeof leaf?.signature === 'string' ? leaf.signature : integrations?.sms?.signature;
265
+ return typeof signature === 'string' ? { ...resolved, signature } : resolved;
266
+ };
@@ -100,7 +100,7 @@ declare global {
100
100
  * exhausts this union (a `switch`, a `Record<FeatureKey, …>`) still has to
101
101
  * handle the new member in the same change.
102
102
  */
103
- type FeatureKey = 'maxOrdersMonth' | 'maxInvoicesMonth' | 'maxProducts' | 'maxUsers' | 'maxCustomers' | 'maxWarehouses' | 'priceListsMax' | 'webhooks' | 'afipInvoicing' | 'suppliers' | 'domesticPayments' | 'stripePayments' | 'cashManagement' | 'importExport' | 'advancedReports' | 'customBranding' | 'prioritySupport' | 'aiFeatures' | 'mobileApp' | 'customDomain' | 'advancedPricing' | 'marketplaceChannels' | 'storefront' | 'loyalty' | 'marketing' | 'mandatory2fa';
103
+ type FeatureKey = 'maxOrdersMonth' | 'maxInvoicesMonth' | 'maxProducts' | 'maxUsers' | 'maxCustomers' | 'maxWarehouses' | 'priceListsMax' | 'webhooks' | 'monthlyMessageAllowance' | 'afipInvoicing' | 'suppliers' | 'domesticPayments' | 'stripePayments' | 'cashManagement' | 'importExport' | 'advancedReports' | 'customBranding' | 'prioritySupport' | 'aiFeatures' | 'mobileApp' | 'customDomain' | 'advancedPricing' | 'marketplaceChannels' | 'storefront' | 'loyalty' | 'marketing' | 'mandatory2fa';
104
104
  /** Resolved entitlements for a specific tenant (matrix + overrides applied). */
105
105
  type ResolvedEntitlements = Record<FeatureKey, Entitlement>;
106
106
  /**
@@ -1238,7 +1238,7 @@ export declare const isPaidPlanTier: (value: unknown) => value is PaidPlanTier;
1238
1238
  * metered / lifetime cap PASSES THROUGH — a new cap silently unbounded.
1239
1239
  * Checklist: `docs/PLANS_AND_ENTITLEMENTS.md` in the api repo.
1240
1240
  */
1241
- export declare const FEATURE_KEYS: readonly ["afipInvoicing", "suppliers", "advancedReports", "cashManagement", "importExport", "domesticPayments", "stripePayments", "aiFeatures", "mobileApp", "customBranding", "customDomain", "prioritySupport", "advancedPricing", "marketplaceChannels", "storefront", "loyalty", "marketing", "mandatory2fa", "maxUsers", "maxProducts", "maxCustomers", "maxWarehouses", "priceListsMax", "webhooks", "maxOrdersMonth", "maxInvoicesMonth"];
1241
+ export declare const FEATURE_KEYS: readonly ["afipInvoicing", "suppliers", "advancedReports", "cashManagement", "importExport", "domesticPayments", "stripePayments", "aiFeatures", "mobileApp", "customBranding", "customDomain", "prioritySupport", "advancedPricing", "marketplaceChannels", "storefront", "loyalty", "marketing", "mandatory2fa", "maxUsers", "maxProducts", "maxCustomers", "maxWarehouses", "priceListsMax", "webhooks", "maxOrdersMonth", "maxInvoicesMonth", "monthlyMessageAllowance"];
1242
1242
  type AssertNoUncoveredFeatureKey<T extends never> = T;
1243
1243
  export type UncoveredFeatureKeys = AssertNoUncoveredFeatureKey<Exclude<FeatureKey, (typeof FEATURE_KEYS)[number]>>;
1244
1244
  export {};
@@ -128,4 +128,5 @@ export const FEATURE_KEYS = [
128
128
  'webhooks',
129
129
  'maxOrdersMonth',
130
130
  'maxInvoicesMonth',
131
+ 'monthlyMessageAllowance',
131
132
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platform-types",
3
- "version": "1.10.836",
3
+ "version": "1.10.838",
4
4
  "description": "Shared wire and entity contracts for the platform.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",