@asteby/storefront 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -177,6 +177,26 @@ Guadalajara) ordered by haversine; it knows the centroid of a few postal codes
177
177
  (`MOCK_POSTAL_CENTROIDS`), pickup is offered only where `pickup.enabled`, and
178
178
  the pickup `fee` is charged on the cart.
179
179
 
180
+ ## Availability (Storefront API 1.1)
181
+
182
+ The server resolves the state (`availability.status`: `in_stock | low_stock | out_of_stock | backorder | preorder`)
183
+ and filters the quantity by the shop's `show_stock`; the storefront never calculates it nor invents a number.
184
+
185
+ ```ts
186
+ import { availabilityView, resolveAvailability, cartLineProblemView } from '@asteby/storefront'
187
+
188
+ const a = resolveAvailability(variant) // server `availability`, or polyfill from available_for_sale / quantity_available
189
+ const view = availabilityView(a, 'es-MX') // { tone, label, sublabel, ctaLabel, ctaEnabled, showNotify, srText, maxPerOrder, showOnCard }
190
+ ```
191
+
192
+ - Texts are es/en by key (`availabilityMessages(locale, overrides)`); a shop overrides texts, never the logic.
193
+ - The "Quedan N" threshold comes from the server (`availability.quantity` only arrives when it applies). The polyfill
194
+ only reports `low_stock` if you pass the shop's own `lowStockThreshold`.
195
+ - `409 insufficient_stock`: `error.stockLines` lists `{ line_id, variant_id, sku, status, max_quantity }`; the cart is untouched.
196
+ `CartLine.problem` is computed when the cart is read.
197
+ - `client.restock.subscribe({ sku, email | phone, locale, consent: true })` -> `202 { status: 'pending' }` ("Avísame cuando vuelva").
198
+ - `schemaAvailability(status)` returns the schema.org `ItemAvailability` URL.
199
+
180
200
  ## Mockup data (`@asteby/storefront/mockup`)
181
201
 
182
202
  Pure helpers behind the fallback mockups of `@asteby/storefront-react/mockups`
@@ -197,6 +217,26 @@ part numbers) → words in `product_type`, category and tags (never the title)
197
217
  options, variant), never prices, and never fill a missing value; every string
198
218
  is cleaned and capped (`MOCKUP_TEXT_LIMITS`).
199
219
 
220
+ ## Embedded payment and promotions (Storefront API 1.2)
221
+
222
+ Types and pure presentation helpers; the React components live in `@asteby/storefront-react`.
223
+
224
+ ```ts
225
+ const cfg = await sf.checkouts.paymentConfig(checkoutId) // payable, reason, amount, client, action (voucher/3DS pendiente tras recargar)
226
+ const methods = await sf.carts.paymentMethods(cart.id) // methods[].options / .client (public data to mount the form)
227
+ const checkout = await sf.carts.checkout(cart.id, { payment_method: 'mercadopago', return_url, payment_flow: 'embedded', payment_option: 'card', expected_total: cart.cost.total })
228
+ const result = await sf.checkouts.pay(checkout.checkout.id, { method: 'card', provider_data: { token }, expected_total: cart.cost.total },
229
+ { idempotencyKey: paymentIdempotencyKey(checkout.checkout.id, attemptId) }) // idempotencyKey is REQUIRED (throws before calling)
230
+ const outcome = paymentOutcome(result, 'es-MX') // approved | in_review | voucher | challenge | redirect | rejected | expired | cancelled
231
+ paymentErrorView(error) // network (retry same attempt) | price_mismatch | payment_in_progress | expired | too_many_attempts
232
+ ```
233
+
234
+ - `declineMessage(code, locale)` / `paymentMessages(locale, overrides)`: actionable es/en texts; unknown codes become `declined`.
235
+ - `safeHttpsUrl`: voucher and redirect URLs are only trusted when https.
236
+ - `sf.carts.recommendations(id, { first, placement })`, `sf.carts.setNote(id, note)`, `sf.promotions.list()`; `Cart.promotions`, `CartCost.discounts[]`.
237
+ - `promoProgressView(cart.promotions, locale)` and `rewardLabel(reward, locale)` only format what OPS sends (`remaining`, `percent`, `reached`); never subtract or divide in the shop.
238
+ - The mock serves `POST /checkouts/:id/payment` with outcomes by token (`tok_APRO` paid, `tok_FUND`/`tok_SECU`/`tok_EXPI`/`tok_FORM`/`tok_CALL`/`tok_OTHE` rejected, `tok_CONT` in review, `tok_3DS` challenge, `method: 'cash'` voucher), idempotency, `price_mismatch` 422, attempts cap 429 and `409` while another attempt is paid/pending; plus `PUT /carts/:id/note`, `GET /carts/:id/recommendations` and `GET /promotions`.
239
+
200
240
  ## API
201
241
 
202
242
  `createStorefrontClient(options)`:
@@ -0,0 +1,220 @@
1
+ // src/availability.ts
2
+ var ES = {
3
+ "status.in_stock": "Disponible",
4
+ "status.low_stock": "Pocas piezas",
5
+ "status.low_stock_n": "Quedan {n}",
6
+ "status.out_of_stock": "Agotado",
7
+ "status.backorder": "Bajo pedido",
8
+ "status.preorder": "Preventa",
9
+ "sub.backorder_eta": "Se env\xEDa a partir del {date}",
10
+ "sub.preorder_date": "Sale a la venta el {date}",
11
+ "cta.add": "Agregar al carrito",
12
+ "cta.out_of_stock": "Agotado",
13
+ "cta.backorder": "Pedir bajo pedido",
14
+ "cta.preorder": "Apartar en preventa",
15
+ "sr.in_stock": "Disponible",
16
+ "sr.low_stock": "Quedan pocas piezas",
17
+ "sr.low_stock_n": "Quedan solo {n} piezas",
18
+ "sr.out_of_stock": "Producto agotado",
19
+ "sr.backorder": "Disponible bajo pedido",
20
+ "sr.preorder": "Disponible en preventa",
21
+ "option.sold_out": "agotado",
22
+ "option.backorder": "bajo pedido",
23
+ "option.preorder": "preventa",
24
+ max_per_order: "M\xE1ximo {n} por pedido",
25
+ "notify.title": "Av\xEDsame cuando vuelva",
26
+ "notify.email": "Correo electr\xF3nico",
27
+ "notify.phone": "WhatsApp o tel\xE9fono",
28
+ "notify.consent": "Acepto recibir un aviso cuando vuelva a estar disponible",
29
+ "notify.submit": "Av\xEDsame",
30
+ "notify.done": "Listo. Te avisaremos cuando vuelva a estar disponible.",
31
+ "notify.error": "No pudimos guardar tu aviso. Revisa tus datos e int\xE9ntalo de nuevo.",
32
+ "problem.out_of_stock": "Ya no hay existencias",
33
+ "problem.insufficient_stock": "No hay suficientes piezas para esta cantidad",
34
+ "problem.insufficient_stock_n": "Solo puedes llevar {n}",
35
+ "problem.adjust": "Ajustar a {n}",
36
+ "problem.remove": "Quitar",
37
+ "conflict.title": "Algunos productos cambiaron de existencias",
38
+ "conflict.body": "Revisa las l\xEDneas marcadas. Tu carrito y tus datos se conservaron; ajusta o quita lo que ya no est\xE1 disponible y contin\xFAa.",
39
+ "conflict.dismiss": "Entendido"
40
+ };
41
+ var EN = {
42
+ "status.in_stock": "In stock",
43
+ "status.low_stock": "Low stock",
44
+ "status.low_stock_n": "Only {n} left",
45
+ "status.out_of_stock": "Sold out",
46
+ "status.backorder": "Made to order",
47
+ "status.preorder": "Pre-order",
48
+ "sub.backorder_eta": "Ships from {date}",
49
+ "sub.preorder_date": "Available on {date}",
50
+ "cta.add": "Add to cart",
51
+ "cta.out_of_stock": "Sold out",
52
+ "cta.backorder": "Order now, ships later",
53
+ "cta.preorder": "Pre-order",
54
+ "sr.in_stock": "In stock",
55
+ "sr.low_stock": "Low stock",
56
+ "sr.low_stock_n": "Only {n} left in stock",
57
+ "sr.out_of_stock": "Sold out",
58
+ "sr.backorder": "Available to order, ships later",
59
+ "sr.preorder": "Available for pre-order",
60
+ "option.sold_out": "sold out",
61
+ "option.backorder": "made to order",
62
+ "option.preorder": "pre-order",
63
+ max_per_order: "Maximum {n} per order",
64
+ "notify.title": "Notify me when it is back",
65
+ "notify.email": "Email",
66
+ "notify.phone": "WhatsApp or phone",
67
+ "notify.consent": "I agree to receive a notice when it is back in stock",
68
+ "notify.submit": "Notify me",
69
+ "notify.done": "Done. We will let you know when it is back in stock.",
70
+ "notify.error": "We could not save your request. Check your details and try again.",
71
+ "problem.out_of_stock": "No longer in stock",
72
+ "problem.insufficient_stock": "Not enough stock for this quantity",
73
+ "problem.insufficient_stock_n": "You can take up to {n}",
74
+ "problem.adjust": "Adjust to {n}",
75
+ "problem.remove": "Remove",
76
+ "conflict.title": "Some items changed stock",
77
+ "conflict.body": "Review the marked lines. Your cart and your details were kept; adjust or remove what is no longer available and continue.",
78
+ "conflict.dismiss": "Got it"
79
+ };
80
+ var CATALOG = { es: ES, en: EN };
81
+ function availabilityLocale(locale) {
82
+ return (locale ?? "").toLowerCase().startsWith("en") ? "en" : "es";
83
+ }
84
+ function availabilityMessages(locale, overrides) {
85
+ const base = CATALOG[availabilityLocale(locale)];
86
+ if (!overrides) return base;
87
+ const out = { ...base };
88
+ for (const [k, v] of Object.entries(overrides)) if (typeof v === "string" && k in base) out[k] = v;
89
+ return out;
90
+ }
91
+ function formatAvailabilityMessage(template, values = {}) {
92
+ return template.replace(/\{(\w+)\}/g, (m, k) => k in values ? String(values[k]) : m);
93
+ }
94
+ function formatDate(value, locale) {
95
+ if (!value) return null;
96
+ const d = /^\d{4}-\d{2}-\d{2}$/.test(value) ? /* @__PURE__ */ new Date(`${value}T00:00:00Z`) : new Date(value);
97
+ if (Number.isNaN(d.getTime())) return null;
98
+ try {
99
+ return new Intl.DateTimeFormat(locale || "es", { dateStyle: "long", timeZone: "UTC" }).format(d);
100
+ } catch {
101
+ return value.slice(0, 10);
102
+ }
103
+ }
104
+ function resolveAvailability(source, options = {}) {
105
+ const a = source.availability;
106
+ if (a && typeof a === "object" && typeof a.status === "string") return a;
107
+ const qty = typeof source.quantity_available === "number" ? source.quantity_available : null;
108
+ const sellable = source.available_for_sale && (qty === null || qty > 0);
109
+ const threshold = options.lowStockThreshold;
110
+ const low = sellable && qty !== null && typeof threshold === "number" && threshold > 0 && qty <= threshold;
111
+ const status = !sellable ? "out_of_stock" : low ? "low_stock" : "in_stock";
112
+ return {
113
+ status,
114
+ quantity: sellable ? qty : null,
115
+ max_per_order: sellable ? qty : null,
116
+ restock_eta: null,
117
+ preorder_release_at: null,
118
+ // Old servers have no subscription route: never offer a form that cannot work.
119
+ notify_available: false
120
+ };
121
+ }
122
+ var TONES = {
123
+ in_stock: "success",
124
+ low_stock: "warning",
125
+ out_of_stock: "danger",
126
+ backorder: "info",
127
+ preorder: "info"
128
+ };
129
+ function availabilityView(availability, locale, options = {}) {
130
+ const m = availabilityMessages(locale, options.messages);
131
+ const known = ["in_stock", "low_stock", "out_of_stock", "backorder", "preorder"].includes(availability.status);
132
+ const status = known ? availability.status : "out_of_stock";
133
+ const qty = typeof availability.quantity === "number" && availability.quantity > 0 ? availability.quantity : null;
134
+ const f = formatAvailabilityMessage;
135
+ let label;
136
+ let sr;
137
+ let cta;
138
+ let sublabel = null;
139
+ let enabled = true;
140
+ switch (status) {
141
+ case "in_stock":
142
+ label = m["status.in_stock"];
143
+ sr = m["sr.in_stock"];
144
+ cta = m["cta.add"];
145
+ break;
146
+ case "low_stock":
147
+ label = qty !== null ? f(m["status.low_stock_n"], { n: qty }) : m["status.low_stock"];
148
+ sr = qty !== null ? f(m["sr.low_stock_n"], { n: qty }) : m["sr.low_stock"];
149
+ cta = m["cta.add"];
150
+ break;
151
+ case "backorder": {
152
+ label = m["status.backorder"];
153
+ sr = m["sr.backorder"];
154
+ cta = m["cta.backorder"];
155
+ const date = formatDate(availability.restock_eta, locale);
156
+ if (date) sublabel = f(m["sub.backorder_eta"], { date });
157
+ break;
158
+ }
159
+ case "preorder": {
160
+ label = m["status.preorder"];
161
+ sr = m["sr.preorder"];
162
+ cta = m["cta.preorder"];
163
+ const date = formatDate(availability.preorder_release_at ?? availability.restock_eta, locale);
164
+ if (date) sublabel = f(m["sub.preorder_date"], { date });
165
+ break;
166
+ }
167
+ default:
168
+ label = m["status.out_of_stock"];
169
+ sr = m["sr.out_of_stock"];
170
+ cta = m["cta.out_of_stock"];
171
+ enabled = false;
172
+ }
173
+ return {
174
+ status,
175
+ tone: TONES[status],
176
+ label,
177
+ sublabel,
178
+ ctaLabel: cta,
179
+ ctaEnabled: enabled,
180
+ showNotify: status === "out_of_stock" && availability.notify_available === true,
181
+ srText: sublabel ? `${sr}. ${sublabel}` : sr,
182
+ maxPerOrder: typeof availability.max_per_order === "number" && availability.max_per_order > 0 ? availability.max_per_order : null,
183
+ showOnCard: status !== "in_stock"
184
+ };
185
+ }
186
+ function cartLineProblemView(problem, locale, options = {}) {
187
+ const m = availabilityMessages(locale, options.messages);
188
+ const code = "code" in problem ? problem.code : problem.status === "out_of_stock" ? "out_of_stock" : "insufficient_stock";
189
+ const max = typeof problem.max_quantity === "number" && problem.max_quantity >= 0 ? problem.max_quantity : null;
190
+ const canAdjust = max !== null && max > 0 && (options.lineQuantity === void 0 || max !== options.lineQuantity);
191
+ const message = code === "out_of_stock" || max === 0 ? m["problem.out_of_stock"] : max !== null ? formatAvailabilityMessage(m["problem.insufficient_stock_n"], { n: max }) : m["problem.insufficient_stock"];
192
+ return {
193
+ code,
194
+ message,
195
+ adjustTo: canAdjust ? max : null,
196
+ adjustLabel: canAdjust ? formatAvailabilityMessage(m["problem.adjust"], { n: max }) : null,
197
+ removeLabel: m["problem.remove"]
198
+ };
199
+ }
200
+ var SCHEMA_AVAILABILITY = {
201
+ in_stock: "https://schema.org/InStock",
202
+ low_stock: "https://schema.org/LimitedAvailability",
203
+ out_of_stock: "https://schema.org/OutOfStock",
204
+ backorder: "https://schema.org/BackOrder",
205
+ preorder: "https://schema.org/PreOrder"
206
+ };
207
+ function schemaAvailability(status) {
208
+ return SCHEMA_AVAILABILITY[status] ?? SCHEMA_AVAILABILITY.out_of_stock;
209
+ }
210
+ function productAvailability(product, options = {}) {
211
+ if (product.availability) return product.availability;
212
+ const all = product.variants.map((v) => resolveAvailability(v, options));
213
+ const rank = ["in_stock", "low_stock", "backorder", "preorder", "out_of_stock"];
214
+ const best = all.slice().sort((a, b) => rank.indexOf(a.status) - rank.indexOf(b.status))[0];
215
+ return best ?? resolveAvailability(product, options);
216
+ }
217
+
218
+ export { availabilityLocale, availabilityMessages, availabilityView, cartLineProblemView, formatAvailabilityMessage, productAvailability, resolveAvailability, schemaAvailability };
219
+ //# sourceMappingURL=chunk-HBSB7XS2.js.map
220
+ //# sourceMappingURL=chunk-HBSB7XS2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/availability.ts"],"names":[],"mappings":"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","file":"chunk-HBSB7XS2.js","sourcesContent":["/**\n * Availability presentation: the single source of the texts and states a\n * storefront shows for stock. Pure functions, no DOM: safe in loaders, SSR and\n * edge runtimes.\n *\n * The server decides the state (`Availability.status`, already filtered by the\n * shop's `show_stock`); this module only turns it into labels. Shops may\n * override texts by key (`messages`) but never the logic. Numbers shown here\n * (\"Quedan 3\") come from `availability.quantity`, which the server sends only\n * when the shop allows it and past its own threshold: there is no threshold in\n * this code.\n */\nimport type { Availability, AvailabilityStatus, CartLineProblem, InsufficientStockLine, Product, ProductVariant } from './types.js'\n\nexport type AvailabilityTone = 'success' | 'warning' | 'danger' | 'info'\n\nexport type AvailabilityLocale = 'es' | 'en'\n\n/** Text keys, overridable per shop. `{n}` and `{date}` are replaced. */\nexport type AvailabilityMessageKey =\n | 'status.in_stock'\n | 'status.low_stock'\n | 'status.low_stock_n'\n | 'status.out_of_stock'\n | 'status.backorder'\n | 'status.preorder'\n | 'sub.backorder_eta'\n | 'sub.preorder_date'\n | 'cta.add'\n | 'cta.out_of_stock'\n | 'cta.backorder'\n | 'cta.preorder'\n | 'sr.in_stock'\n | 'sr.low_stock'\n | 'sr.low_stock_n'\n | 'sr.out_of_stock'\n | 'sr.backorder'\n | 'sr.preorder'\n | 'option.sold_out'\n | 'option.backorder'\n | 'option.preorder'\n | 'max_per_order'\n | 'notify.title'\n | 'notify.email'\n | 'notify.phone'\n | 'notify.consent'\n | 'notify.submit'\n | 'notify.done'\n | 'notify.error'\n | 'problem.out_of_stock'\n | 'problem.insufficient_stock'\n | 'problem.insufficient_stock_n'\n | 'problem.adjust'\n | 'problem.remove'\n | 'conflict.title'\n | 'conflict.body'\n | 'conflict.dismiss'\n\nexport type AvailabilityMessages = Record<AvailabilityMessageKey, string>\n\nconst ES: AvailabilityMessages = {\n 'status.in_stock': 'Disponible',\n 'status.low_stock': 'Pocas piezas',\n 'status.low_stock_n': 'Quedan {n}',\n 'status.out_of_stock': 'Agotado',\n 'status.backorder': 'Bajo pedido',\n 'status.preorder': 'Preventa',\n 'sub.backorder_eta': 'Se envía a partir del {date}',\n 'sub.preorder_date': 'Sale a la venta el {date}',\n 'cta.add': 'Agregar al carrito',\n 'cta.out_of_stock': 'Agotado',\n 'cta.backorder': 'Pedir bajo pedido',\n 'cta.preorder': 'Apartar en preventa',\n 'sr.in_stock': 'Disponible',\n 'sr.low_stock': 'Quedan pocas piezas',\n 'sr.low_stock_n': 'Quedan solo {n} piezas',\n 'sr.out_of_stock': 'Producto agotado',\n 'sr.backorder': 'Disponible bajo pedido',\n 'sr.preorder': 'Disponible en preventa',\n 'option.sold_out': 'agotado',\n 'option.backorder': 'bajo pedido',\n 'option.preorder': 'preventa',\n max_per_order: 'Máximo {n} por pedido',\n 'notify.title': 'Avísame cuando vuelva',\n 'notify.email': 'Correo electrónico',\n 'notify.phone': 'WhatsApp o teléfono',\n 'notify.consent': 'Acepto recibir un aviso cuando vuelva a estar disponible',\n 'notify.submit': 'Avísame',\n 'notify.done': 'Listo. Te avisaremos cuando vuelva a estar disponible.',\n 'notify.error': 'No pudimos guardar tu aviso. Revisa tus datos e inténtalo de nuevo.',\n 'problem.out_of_stock': 'Ya no hay existencias',\n 'problem.insufficient_stock': 'No hay suficientes piezas para esta cantidad',\n 'problem.insufficient_stock_n': 'Solo puedes llevar {n}',\n 'problem.adjust': 'Ajustar a {n}',\n 'problem.remove': 'Quitar',\n 'conflict.title': 'Algunos productos cambiaron de existencias',\n 'conflict.body': 'Revisa las líneas marcadas. Tu carrito y tus datos se conservaron; ajusta o quita lo que ya no está disponible y continúa.',\n 'conflict.dismiss': 'Entendido',\n}\n\nconst EN: AvailabilityMessages = {\n 'status.in_stock': 'In stock',\n 'status.low_stock': 'Low stock',\n 'status.low_stock_n': 'Only {n} left',\n 'status.out_of_stock': 'Sold out',\n 'status.backorder': 'Made to order',\n 'status.preorder': 'Pre-order',\n 'sub.backorder_eta': 'Ships from {date}',\n 'sub.preorder_date': 'Available on {date}',\n 'cta.add': 'Add to cart',\n 'cta.out_of_stock': 'Sold out',\n 'cta.backorder': 'Order now, ships later',\n 'cta.preorder': 'Pre-order',\n 'sr.in_stock': 'In stock',\n 'sr.low_stock': 'Low stock',\n 'sr.low_stock_n': 'Only {n} left in stock',\n 'sr.out_of_stock': 'Sold out',\n 'sr.backorder': 'Available to order, ships later',\n 'sr.preorder': 'Available for pre-order',\n 'option.sold_out': 'sold out',\n 'option.backorder': 'made to order',\n 'option.preorder': 'pre-order',\n max_per_order: 'Maximum {n} per order',\n 'notify.title': 'Notify me when it is back',\n 'notify.email': 'Email',\n 'notify.phone': 'WhatsApp or phone',\n 'notify.consent': 'I agree to receive a notice when it is back in stock',\n 'notify.submit': 'Notify me',\n 'notify.done': 'Done. We will let you know when it is back in stock.',\n 'notify.error': 'We could not save your request. Check your details and try again.',\n 'problem.out_of_stock': 'No longer in stock',\n 'problem.insufficient_stock': 'Not enough stock for this quantity',\n 'problem.insufficient_stock_n': 'You can take up to {n}',\n 'problem.adjust': 'Adjust to {n}',\n 'problem.remove': 'Remove',\n 'conflict.title': 'Some items changed stock',\n 'conflict.body': 'Review the marked lines. Your cart and your details were kept; adjust or remove what is no longer available and continue.',\n 'conflict.dismiss': 'Got it',\n}\n\nconst CATALOG: Record<AvailabilityLocale, AvailabilityMessages> = { es: ES, en: EN }\n\n/** `es-MX` -> `es`, `en-US` -> `en`; anything else falls back to Spanish. */\nexport function availabilityLocale(locale: string | null | undefined): AvailabilityLocale {\n return (locale ?? '').toLowerCase().startsWith('en') ? 'en' : 'es'\n}\n\n/** Catalog for a locale with the shop's per-key overrides applied. */\nexport function availabilityMessages(locale: string | null | undefined, overrides?: Partial<AvailabilityMessages> | null): AvailabilityMessages {\n const base = CATALOG[availabilityLocale(locale)]\n if (!overrides) return base\n const out = { ...base }\n for (const [k, v] of Object.entries(overrides)) if (typeof v === 'string' && k in base) out[k as AvailabilityMessageKey] = v\n return out\n}\n\n/** Replaces `{name}` placeholders. */\nexport function formatAvailabilityMessage(template: string, values: Record<string, string | number> = {}): string {\n return template.replace(/\\{(\\w+)\\}/g, (m, k: string) => (k in values ? String(values[k]) : m))\n}\n\nfunction formatDate(value: string | null | undefined, locale: string | null | undefined): string | null {\n if (!value) return null\n const d = /^\\d{4}-\\d{2}-\\d{2}$/.test(value) ? new Date(`${value}T00:00:00Z`) : new Date(value)\n if (Number.isNaN(d.getTime())) return null\n try {\n // UTC keeps server and client renders identical (no hydration mismatch).\n return new Intl.DateTimeFormat(locale || 'es', { dateStyle: 'long', timeZone: 'UTC' }).format(d)\n } catch {\n return value.slice(0, 10)\n }\n}\n\nexport type ResolveAvailabilityOptions = {\n /**\n * Polyfill only: shop-level \"low stock\" threshold to derive `low_stock` from\n * `quantity_available` on servers older than contract 1.1. Without it the\n * polyfill never reports `low_stock`. Ignored when the server sends `availability`.\n */\n lowStockThreshold?: number | null\n}\n\ntype AvailabilitySource = Pick<Product | ProductVariant, 'available_for_sale'> & {\n availability?: Availability | null\n quantity_available?: number | null\n}\n\n/**\n * The `Availability` of a variant or product. Uses the server's `availability`\n * when present; otherwise derives `in_stock | low_stock | out_of_stock` from\n * `available_for_sale` / `quantity_available` so storefronts can migrate before\n * the backend does. A malformed `availability` falls back to the polyfill.\n */\nexport function resolveAvailability(source: AvailabilitySource, options: ResolveAvailabilityOptions = {}): Availability {\n const a = source.availability\n if (a && typeof a === 'object' && typeof a.status === 'string') return a\n const qty = typeof source.quantity_available === 'number' ? source.quantity_available : null\n const sellable = source.available_for_sale && (qty === null || qty > 0)\n const threshold = options.lowStockThreshold\n const low = sellable && qty !== null && typeof threshold === 'number' && threshold > 0 && qty <= threshold\n const status: AvailabilityStatus = !sellable ? 'out_of_stock' : low ? 'low_stock' : 'in_stock'\n return {\n status,\n quantity: sellable ? qty : null,\n max_per_order: sellable ? qty : null,\n restock_eta: null,\n preorder_release_at: null,\n // Old servers have no subscription route: never offer a form that cannot work.\n notify_available: false,\n }\n}\n\nexport type AvailabilityView = {\n status: AvailabilityStatus\n tone: AvailabilityTone\n /** Short badge text: \"Agotado\", \"Quedan 3\", \"Bajo pedido\"... Never empty. */\n label: string\n /** ETA / release date text, or `null`. */\n sublabel: string | null\n /** Primary button text. */\n ctaLabel: string\n /** The primary button may be pressed (backorder/preorder are purchasable). */\n ctaEnabled: boolean\n /** Offer the \"Notify me when it is back\" form. */\n showNotify: boolean\n /** Full sentence for screen readers. */\n srText: string\n /** Units to cap the quantity picker at, or `null`. */\n maxPerOrder: number | null\n /** Show a badge on listing cards (everything but plain `in_stock`: do not clutter). */\n showOnCard: boolean\n}\n\nexport type AvailabilityViewOptions = {\n messages?: Partial<AvailabilityMessages> | null\n}\n\nconst TONES: Record<AvailabilityStatus, AvailabilityTone> = {\n in_stock: 'success',\n low_stock: 'warning',\n out_of_stock: 'danger',\n backorder: 'info',\n preorder: 'info',\n}\n\n/**\n * Texts and flags for one availability. `availability` accepts anything with an\n * unknown `status` (a future server): it is treated as `out_of_stock`.\n */\nexport function availabilityView(availability: Availability, locale: string | null | undefined, options: AvailabilityViewOptions = {}): AvailabilityView {\n const m = availabilityMessages(locale, options.messages)\n const known = (['in_stock', 'low_stock', 'out_of_stock', 'backorder', 'preorder'] as const).includes(availability.status)\n const status: AvailabilityStatus = known ? availability.status : 'out_of_stock'\n const qty = typeof availability.quantity === 'number' && availability.quantity > 0 ? availability.quantity : null\n const f = formatAvailabilityMessage\n\n let label: string\n let sr: string\n let cta: string\n let sublabel: string | null = null\n let enabled = true\n switch (status) {\n case 'in_stock':\n label = m['status.in_stock']\n sr = m['sr.in_stock']\n cta = m['cta.add']\n break\n case 'low_stock':\n // Without a visible quantity the server still says \"low\": no number is invented.\n label = qty !== null ? f(m['status.low_stock_n'], { n: qty }) : m['status.low_stock']\n sr = qty !== null ? f(m['sr.low_stock_n'], { n: qty }) : m['sr.low_stock']\n cta = m['cta.add']\n break\n case 'backorder': {\n label = m['status.backorder']\n sr = m['sr.backorder']\n cta = m['cta.backorder']\n const date = formatDate(availability.restock_eta, locale)\n if (date) sublabel = f(m['sub.backorder_eta'], { date })\n break\n }\n case 'preorder': {\n label = m['status.preorder']\n sr = m['sr.preorder']\n cta = m['cta.preorder']\n const date = formatDate(availability.preorder_release_at ?? availability.restock_eta, locale)\n if (date) sublabel = f(m['sub.preorder_date'], { date })\n break\n }\n default:\n label = m['status.out_of_stock']\n sr = m['sr.out_of_stock']\n cta = m['cta.out_of_stock']\n enabled = false\n }\n return {\n status,\n tone: TONES[status],\n label,\n sublabel,\n ctaLabel: cta,\n ctaEnabled: enabled,\n showNotify: status === 'out_of_stock' && availability.notify_available === true,\n srText: sublabel ? `${sr}. ${sublabel}` : sr,\n maxPerOrder: typeof availability.max_per_order === 'number' && availability.max_per_order > 0 ? availability.max_per_order : null,\n showOnCard: status !== 'in_stock',\n }\n}\n\nexport type CartLineProblemView = {\n code: CartLineProblem['code']\n /** \"Ya no hay existencias\" / \"Solo puedes llevar 2\". */\n message: string\n /** Quantity to adjust the line to, or `null` (remove only). */\n adjustTo: number | null\n adjustLabel: string | null\n removeLabel: string\n}\n\n/** Texts and actions for a cart line problem. Offers \"Adjust to N\" only when `max_quantity` is a positive number different from the line quantity (a refused change leaves the cart at its old quantity). */\nexport function cartLineProblemView(\n problem: CartLineProblem | InsufficientStockLine,\n locale: string | null | undefined,\n options: AvailabilityViewOptions & { lineQuantity?: number } = {},\n): CartLineProblemView {\n const m = availabilityMessages(locale, options.messages)\n const code: CartLineProblem['code'] = 'code' in problem ? problem.code : problem.status === 'out_of_stock' ? 'out_of_stock' : 'insufficient_stock'\n const max = typeof problem.max_quantity === 'number' && problem.max_quantity >= 0 ? problem.max_quantity : null\n const canAdjust = max !== null && max > 0 && (options.lineQuantity === undefined || max !== options.lineQuantity)\n const message =\n code === 'out_of_stock' || max === 0\n ? m['problem.out_of_stock']\n : max !== null\n ? formatAvailabilityMessage(m['problem.insufficient_stock_n'], { n: max })\n : m['problem.insufficient_stock']\n return {\n code,\n message,\n adjustTo: canAdjust ? max : null,\n adjustLabel: canAdjust ? formatAvailabilityMessage(m['problem.adjust'], { n: max }) : null,\n removeLabel: m['problem.remove'],\n }\n}\n\nconst SCHEMA_AVAILABILITY: Record<AvailabilityStatus, string> = {\n in_stock: 'https://schema.org/InStock',\n low_stock: 'https://schema.org/LimitedAvailability',\n out_of_stock: 'https://schema.org/OutOfStock',\n backorder: 'https://schema.org/BackOrder',\n preorder: 'https://schema.org/PreOrder',\n}\n\n/** schema.org `ItemAvailability` URL for a status (unknown -> `OutOfStock`). */\nexport function schemaAvailability(status: AvailabilityStatus | string): string {\n return SCHEMA_AVAILABILITY[status as AvailabilityStatus] ?? SCHEMA_AVAILABILITY.out_of_stock\n}\n\n/** Best state among variants for a product summary (when the server sends none). */\nexport function productAvailability(product: Pick<Product, 'variants' | 'available_for_sale' | 'availability'>, options: ResolveAvailabilityOptions = {}): Availability {\n if (product.availability) return product.availability\n const all = product.variants.map((v) => resolveAvailability(v, options))\n const rank: AvailabilityStatus[] = ['in_stock', 'low_stock', 'backorder', 'preorder', 'out_of_stock']\n const best = all.slice().sort((a, b) => rank.indexOf(a.status) - rank.indexOf(b.status))[0]\n return best ?? resolveAvailability(product, options)\n}\n"]}