@pcreative/commerce-contract 1.0.0 → 1.2.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.
@@ -1,86 +1,26 @@
1
- /**
2
- * Adaptador HTTP del contrato de datos.
3
- *
4
- * Habla con la API de tienda por `fetch` a pelo, sin SDK. No es purismo: un SDK
5
- * trae su propio manejo de sesión y presupone un entorno de navegador, y eso es
6
- * justo lo que impedía que un tema Astro, un runtime edge o una app nativa
7
- * usaran el mismo código. `fetch` está en todos.
8
- *
9
- * import { createCommerce } from "@pcreative/commerce-contract/api"
10
- * const commerce = createCommerce({
11
- * baseUrl: process.env.COMMERCE_URL,
12
- * publishableKey: process.env.COMMERCE_KEY,
13
- * })
14
- */
15
1
  import { money } from "../index.js"
16
2
 
17
- const PRODUCT_FIELDS = [
18
- "id", "title", "subtitle", "handle", "description", "thumbnail", "metadata",
19
- "*images", "*categories", "*tags", "*variants", "*variants.calculated_price",
20
- "+variants.inventory_quantity", "variants.options.value", "variants.options.option.title",
21
- // Marketplace: quién vende el producto. Con `+` porque no es un campo de
22
- // serie del catálogo — sale de un enlace entre módulos. Si la instalación no
23
- // tiene marketplace, sencillamente no viene nada y `seller` queda vacío.
24
- "+vendedor.id", "+vendedor.nombre", "+vendedor.slug",
25
- ].join(",")
26
-
27
- const CART_FIELDS =
28
- "*items,*items.variant,*items.product,*region,*shipping_address,*billing_address,*shipping_methods,*promotions,*payment_collection,*payment_collection.payment_sessions"
3
+ const ORDEN_DEL_CONTRATO = {
4
+ newest: "novedades",
5
+ price_asc: "precio_asc",
6
+ price_desc: "precio_desc",
7
+ }
29
8
 
30
9
  export function createCommerce({
31
10
  baseUrl,
32
- /**
33
- * Usar el catálogo PROPIO (`/tienda/*`) en vez del del motor (`/store/*`).
34
- *
35
- * ── 🔴 POR QUÉ AHORA VIENE ENCENDIDO ────────────────────────────────────
36
- *
37
- * Venía APAGADO, y era el mismo fallo que ya se arregló en el panel el
38
- * 2026-08-18 sin arreglarlo aquí. Aquel día `npm start` pasó a levantar el
39
- * servidor propio, que sirve `/tienda/*` y NO sirve `/store/*`.
40
- *
41
- * Con el interruptor apagado por defecto, cualquier tema recién hecho pedía
42
- * el catálogo al motor, recibía un 404 y salía con la tienda VACÍA. Y no
43
- * daba un error: pintaba la portada entera, bien maquetada, sin un solo
44
- * producto. Se descubrió probando un tema antes de publicarlo.
45
- *
46
- * Un valor por defecto que solo funciona si alguien se acuerda de cambiarlo
47
- * no es un valor por defecto.
48
- *
49
- * Se pone a `false` a propósito para volver al motor, y va emparejado con
50
- * `npm run start:motor` en el backend. Uno sin el otro es media vuelta
51
- * atrás, que es peor que ninguna.
52
- */
53
- catalogoPropio = true,
54
11
  publishableKey,
55
- /** Código de país de la región por defecto, p.ej. "es". */
56
12
  countryCode,
57
- /**
58
- * Idioma en el que se quiere el contenido, p.ej. "en-US".
59
- *
60
- * Va en una cabecera y no en cada llamada a propósito: el idioma es del
61
- * VISITANTE, no de la consulta. Si hubiera que pasarlo función por función,
62
- * bastaría con que un tema se olvidara en una sola pantalla para que esa
63
- * página saliera en otro idioma — y ese fallo es de los que no se ven hasta
64
- * que lo dice un cliente.
65
- *
66
- * Lo que no esté traducido vuelve en el idioma original, así que poner un
67
- * idioma nunca deja la tienda vacía.
68
- */
69
13
  locale,
70
- /** `fetch` alternativo (para tests o para inyectar caché del framework). */
71
14
  fetch: fetchImpl,
72
- /** Opciones extra por petición: `next: { revalidate }` en Next, `cache` en el edge… */
73
15
  requestInit = {},
74
16
  } = {}) {
75
17
  if (!baseUrl) throw new Error("createCommerce: falta baseUrl")
76
18
  const f = fetchImpl ?? globalThis.fetch
77
19
  const raiz = String(baseUrl).replace(/\/$/, "")
78
20
 
79
- let regionPromesa = null
80
-
81
21
  async function api(ruta, { method = "GET", body, token, ...extra } = {}) {
82
22
  const cabeceras = { accept: "application/json", ...(extra.headers ?? {}) }
83
- if (publishableKey) cabeceras["x-publishable-api-key"] = publishableKey
23
+ if (publishableKey) cabeceras["x-pcc-clave"] = publishableKey
84
24
  if (locale) cabeceras["x-pcc-locale"] = locale
85
25
  if (body !== undefined) cabeceras["content-type"] = "application/json"
86
26
  if (token) cabeceras.authorization = `Bearer ${token}`
@@ -98,22 +38,20 @@ export function createCommerce({
98
38
  try {
99
39
  detalle = (await res.json())?.message ?? ""
100
40
  } catch {
101
- /* respuesta sin JSON */
41
+ detalle = ""
102
42
  }
103
- // El código HTTP viaja en el error: un 404 de producto no es lo mismo que
104
- // un 401 por clave publicable mal puesta, y el tema debe poder distinguirlo.
105
43
  const err = new Error(`${method} ${ruta} → ${res.status}${detalle ? `: ${detalle}` : ""}`)
106
44
  err.status = res.status
45
+ err.detalle = detalle
107
46
  throw err
108
47
  }
109
48
  return res.status === 204 ? null : res.json()
110
49
  }
111
50
 
112
- /** La región fija moneda e impuestos. Se resuelve una vez y se reutiliza. */
51
+ let regionPromesa = null
52
+
113
53
  function region() {
114
- // Con el catálogo propio, las regiones también salen de nuestras rutas: si
115
- // no, la tienda leería la moneda del motor y los precios del nuestro.
116
- regionPromesa ??= api(catalogoPropio ? "/tienda/regiones" : "/store/regions")
54
+ regionPromesa ??= api("/tienda/regiones")
117
55
  .then(({ regions = [] }) => {
118
56
  if (countryCode) {
119
57
  const r = regions.find((x) => (x.countries ?? []).some((c) => c.iso_2 === countryCode.toLowerCase()))
@@ -125,73 +63,7 @@ export function createCommerce({
125
63
  return regionPromesa
126
64
  }
127
65
 
128
- // ── Mapeo ───────────────────────────────────────────────────────────────
129
-
130
- const monedaDe = (v, fallback) =>
131
- (v?.calculated_price?.currency_code ?? fallback ?? "eur").toLowerCase()
132
-
133
- function mapVariant(v, fallbackCurrency) {
134
- const cp = v?.calculated_price ?? {}
135
- const precio = money(cp.calculated_amount, monedaDe(v, fallbackCurrency), true)
136
- const original = cp.original_amount
137
- return {
138
- id: v.id,
139
- title: v.title ?? "",
140
- sku: v.sku ?? undefined,
141
- barcode: v.barcode ?? undefined,
142
- price: precio,
143
- compareAt:
144
- original != null && original > (cp.calculated_amount ?? 0)
145
- ? money(original, precio.currency, true)
146
- : null,
147
- // `inventory_quantity` solo viene si la variante gestiona inventario;
148
- // si no, no hay número que enseñar y `null` es la respuesta honesta.
149
- stock: v.manage_inventory === false ? null : v.inventory_quantity ?? null,
150
- available:
151
- v.manage_inventory === false ||
152
- v.allow_backorder === true ||
153
- (v.inventory_quantity ?? 0) > 0,
154
- options: (v.options ?? []).map((o) => ({
155
- name: o.option?.title ?? "",
156
- value: o.value ?? "",
157
- })),
158
- weight: v.weight ?? null,
159
- image: null,
160
- }
161
- }
162
-
163
- function mapProduct(p, fallbackCurrency) {
164
- const variants = (p.variants ?? []).map((v) => mapVariant(v, fallbackCurrency))
165
- const conPrecio = variants.filter((v) => v.price.amount > 0)
166
- const minimo = conPrecio.length
167
- ? conPrecio.reduce((a, b) => (b.price.amount < a.price.amount ? b : a))
168
- : variants[0]
169
-
170
- return {
171
- id: p.id,
172
- handle: p.handle,
173
- title: p.title,
174
- subtitle: p.subtitle ?? null,
175
- description: p.description ?? null,
176
- brand: p.metadata?.brand ?? p.metadata?.marca ?? null,
177
- // El enlace puede llegar como objeto o como lista de uno, según por dónde
178
- // se pida. Se normaliza aquí para que el tema no tenga que saberlo.
179
- seller: (() => {
180
- const v = Array.isArray(p.vendedor) ? p.vendedor[0] : p.vendedor
181
- return v?.id ? { id: v.id, name: v.nombre, slug: v.slug } : null
182
- })(),
183
- images: (p.images ?? []).map((i) => ({ url: i.url, alt: p.title })),
184
- thumbnail: p.thumbnail ?? p.images?.[0]?.url ?? null,
185
- price: minimo?.price ?? money(0, fallbackCurrency ?? "eur", true),
186
- variants,
187
- categories: (p.categories ?? []).map((c) => ({ id: c.id, handle: c.handle, name: c.name })),
188
- tags: (p.tags ?? []).map((t) => t.value ?? t.name).filter(Boolean),
189
- rating: null,
190
- reviewCount: 0,
191
- available: variants.some((v) => v.available),
192
- metadata: p.metadata ?? {},
193
- }
194
- }
66
+ const monedaDeLaTienda = async () => String((await region())?.currency_code ?? "eur").toLowerCase()
195
67
 
196
68
  function mapCategory(c) {
197
69
  return {
@@ -205,30 +77,18 @@ export function createCommerce({
205
77
  }
206
78
  }
207
79
 
208
- /**
209
- * Del producto que devuelve NUESTRO catálogo al del contrato.
210
- *
211
- * Viene casi con la forma correcta a propósito: el catálogo propio se
212
- * escribió para hablar el contrato, no el idioma del motor.
213
- */
214
- function mapProductoPropio(p) {
80
+ function mapProducto(p) {
215
81
  return {
216
82
  id: p.id,
217
83
  handle: p.handle,
218
84
  title: p.title,
219
85
  subtitle: p.subtitle ?? null,
220
86
  description: p.description ?? null,
221
- // El `alt` se pone aquí, como en el camino del motor: una imagen sin
222
- // texto alternativo es una ficha que no se puede leer con un lector de
223
- // pantalla y que el buscador no entiende.
224
87
  images: (p.images ?? []).map((i) => ({ url: i.url, alt: i.alt ?? p.title })),
225
- thumbnail: p.thumbnail ?? null,
88
+ thumbnail: p.thumbnail ?? p.images?.[0]?.url ?? null,
226
89
  price: p.price,
227
- // Los cuatro que faltaban. No son adorno: la marca y las etiquetas las
228
- // pinta la ficha, y `reviewCount` decide si sale el bloque de reseñas.
229
- // Al no venir, un tema los leía como `undefined` y dejaba huecos sin
230
- // dar ningún error.
231
90
  brand: p.brand ?? p.metadata?.brand ?? p.metadata?.marca ?? null,
91
+ seller: p.seller ?? null,
232
92
  tags: p.tags ?? [],
233
93
  rating: p.rating ?? null,
234
94
  reviewCount: p.reviewCount ?? 0,
@@ -239,109 +99,40 @@ export function createCommerce({
239
99
  barcode: v.barcode ?? undefined,
240
100
  price: v.price,
241
101
  compareAt: v.compareAt ?? null,
242
- // `null` = esa variante no lleva la cuenta unidad a unidad. NO es cero:
243
- // decir cero sería afirmar que no queda, y el tema lo pintaría agotado.
244
102
  stock: v.stock ?? null,
245
103
  available: v.available,
246
104
  options: v.options ?? [],
105
+ weight: v.weight ?? null,
106
+ image: v.image ?? null,
107
+ images: v.images ?? [],
108
+ tiers: v.tiers ?? [],
109
+ digital: Boolean(v.digital),
110
+ requiresShipping: v.requiresShipping ?? !v.digital,
111
+ bundle: v.bundle ?? null,
112
+ bundleGroups: v.bundleGroups ?? null,
113
+ measure: v.measure ?? null,
114
+ unitPrice: v.unitPrice ?? null,
247
115
  })),
248
116
  categories: p.categories ?? [],
117
+ quantityRules: p.quantityRules ?? null,
118
+ customization: p.customization ?? [],
249
119
  available: p.available,
250
- metadata: p.metadata,
251
- }
252
- }
253
-
254
- function mapLineItem(i, currency) {
255
- return {
256
- id: i.id,
257
- productId: i.product_id ?? i.product?.id ?? "",
258
- variantId: i.variant_id ?? i.variant?.id ?? "",
259
- handle: i.product?.handle ?? i.product_handle ?? undefined,
260
- title: i.product_title ?? i.title ?? "",
261
- variantTitle: i.variant_title ?? i.variant?.title ?? undefined,
262
- thumbnail: i.thumbnail ?? null,
263
- quantity: i.quantity ?? 0,
264
- unitPrice: money(i.unit_price, currency, true),
265
- total: money(i.total ?? (i.unit_price ?? 0) * (i.quantity ?? 0), currency, true),
266
- // Un CARGO, no mercancía: el recargo por forma de pago, por ejemplo. No
267
- // tiene variante y no se envía. El tema lo necesita para no contarlo dos
268
- // veces cuando además lo enseña por separado.
269
- isFee: i.requires_shipping === false && !(i.variant_id ?? i.variant?.id),
270
- }
271
- }
272
-
273
- function mapCart(c) {
274
- if (!c) return null
275
- const currency = (c.currency_code ?? c.region?.currency_code ?? "eur").toLowerCase()
276
- const items = (c.items ?? []).map((i) => mapLineItem(i, currency))
277
- return {
278
- id: c.id,
279
- items,
280
- subtotal: money(c.item_subtotal ?? c.subtotal, currency, true),
281
- shipping: money(c.shipping_total, currency, true),
282
- discount: money(c.discount_total, currency, true),
283
- tax: money(c.tax_total, currency, true),
284
- total: money(c.total, currency, true),
285
- itemCount: items.reduce((n, i) => n + i.quantity, 0),
286
- email: c.email ?? null,
287
- shippingAddress: mapAddress(c.shipping_address),
288
- billingAddress: mapAddress(c.billing_address),
289
- shippingMethods: (c.shipping_methods ?? []).map((m) => ({
290
- id: m.shipping_option_id ?? m.id,
291
- name: m.name ?? "",
292
- price: money(m.amount, currency, true),
293
- })),
294
- regionId: c.region_id ?? c.region?.id,
295
- promoCodes: (c.promotions ?? []).map((p) => p.code).filter(Boolean),
296
- pago: siguientePago(c),
297
- }
298
- }
299
-
300
- /**
301
- * Cómo tiene que seguir el comprador para pagar.
302
- *
303
- * Las pasarelas se reparten en dos familias: unas mandan al comprador a su
304
- * página y otras le hacen confirmar en el navegador con un secreto. Sin esto
305
- * el tema no puede hacer ninguna de las dos, y el comprador se queda mirando
306
- * un botón que no lleva a ningún sitio.
307
- *
308
- * El dato lo pone el proveedor de pago del backend en la sesión. Los métodos
309
- * sin pasarela —transferencia, contrarreembolso— no ponen nada, y entonces
310
- * pagar es simplemente confirmar el pedido.
311
- */
312
- function siguientePago(c) {
313
- const sesiones = c?.payment_collection?.payment_sessions ?? []
314
- // La última pendiente: si alguien cambia de método, la anterior queda ahí
315
- // y usarla mandaría a pagar por una pasarela que ya descartó.
316
- const sesion = [...sesiones].reverse().find((s) => s.status === "pending" || s.status === "requires_more")
317
- const paso = sesion?.data?.siguiente
318
- if (!paso || typeof paso !== "object" || !paso.tipo) return null
319
- if (paso.tipo === "redirigir" && typeof paso.url === "string") {
320
- return { tipo: "redirigir", url: paso.url, proveedor: sesion.provider_id }
321
- }
322
- if (paso.tipo === "confirmar_en_cliente") {
323
- return {
324
- tipo: "confirmar_en_cliente",
325
- pasarela: String(paso.pasarela ?? ""),
326
- datos: paso.datos ?? {},
327
- proveedor: sesion.provider_id,
328
- }
120
+ metadata: p.metadata ?? {},
329
121
  }
330
- return null
331
122
  }
332
123
 
333
124
  function mapAddress(a) {
334
125
  if (!a) return null
335
126
  return {
336
- firstName: a.first_name ?? undefined,
337
- lastName: a.last_name ?? undefined,
127
+ firstName: a.first_name ?? a.firstName ?? undefined,
128
+ lastName: a.last_name ?? a.lastName ?? undefined,
338
129
  company: a.company ?? undefined,
339
- address1: a.address_1 ?? undefined,
340
- address2: a.address_2 ?? undefined,
130
+ address1: a.address_1 ?? a.address1 ?? undefined,
131
+ address2: a.address_2 ?? a.address2 ?? undefined,
341
132
  city: a.city ?? undefined,
342
133
  province: a.province ?? undefined,
343
- postalCode: a.postal_code ?? undefined,
344
- countryCode: a.country_code ?? undefined,
134
+ postalCode: a.postal_code ?? a.postalCode ?? undefined,
135
+ countryCode: a.country_code ?? a.countryCode ?? undefined,
345
136
  phone: a.phone ?? undefined,
346
137
  }
347
138
  }
@@ -359,173 +150,96 @@ export function createCommerce({
359
150
  phone: a.phone,
360
151
  })
361
152
 
362
- function mapOrder(o) {
363
- const currency = (o.currency_code ?? "eur").toLowerCase()
153
+ function mapCart(c) {
154
+ if (!c || c.completedAt) return null
155
+ const items = (c.items ?? []).map((l) => ({
156
+ id: l.id,
157
+ productId: l.productId ?? "",
158
+ variantId: l.variantId ?? "",
159
+ handle: l.handle ?? undefined,
160
+ title: l.title ?? "",
161
+ variantTitle: l.variantTitle ?? undefined,
162
+ thumbnail: l.thumbnail ?? null,
163
+ quantity: l.quantity ?? 0,
164
+ unitPrice: l.unitPrice,
165
+ total: l.total,
166
+ isFee: !l.variantId,
167
+ customization: l.customization ?? null,
168
+ personalized: Boolean(l.personalized),
169
+ digital: Boolean(l.digital),
170
+ bundle: l.bundle ?? null,
171
+ quantityRule: l.quantityRule ?? null,
172
+ tier: l.tier ?? null,
173
+ nextTier: l.nextTier ?? null,
174
+ problem: l.problem ?? null,
175
+ }))
364
176
  return {
365
- id: o.id,
366
- displayId: o.display_id,
367
- email: o.email,
368
- status: o.status ?? "pending",
369
- items: (o.items ?? []).map((i) => mapLineItem(i, currency)),
370
- subtotal: money(o.item_subtotal ?? o.subtotal, currency, true),
371
- shipping: money(o.shipping_total, currency, true),
372
- discount: money(o.discount_total, currency, true),
373
- tax: money(o.tax_total, currency, true),
374
- total: money(o.total, currency, true),
375
- shippingAddress: mapAddress(o.shipping_address),
376
- createdAt: o.created_at,
177
+ id: c.id,
178
+ items,
179
+ subtotal: c.subtotal,
180
+ shipping: c.shipping,
181
+ discount: c.discount,
182
+ tax: c.tax,
183
+ total: c.total,
184
+ itemCount: c.itemCount ?? items.reduce((n, i) => n + i.quantity, 0),
185
+ email: c.email ?? null,
186
+ shippingAddress: mapAddress(c.shippingAddress),
187
+ billingAddress: mapAddress(c.billingAddress),
188
+ shippingMethods: c.shippingMethods ?? [],
189
+ regionId: c.regionId,
190
+ promoCodes: c.promoCodes ?? [],
191
+ hasDigital: Boolean(c.hasDigital),
192
+ digitalConsent: c.digitalConsent ?? null,
193
+ pago: c.pago ?? null,
194
+ vatNumber: c.vatNumber ?? null,
195
+ taxExempt: c.taxExempt ?? null,
377
196
  }
378
197
  }
379
198
 
380
- // ── Catálogo ────────────────────────────────────────────────────────────
381
-
382
199
  async function listProducts(params = {}) {
383
- if (catalogoPropio) {
384
- const q = new URLSearchParams()
385
- if (params.limit) q.set("limite", String(params.limit))
386
- if (params.offset) q.set("desde", String(params.offset))
387
- if (params.categoryId) q.set("categoria", params.categoryId)
388
- // 🔴 EL TÉRMINO DE BÚSQUEDA. Sin esta línea `search()` devolvía el
389
- // CATÁLOGO ENTERO para cualquier cosa que se escribiera: el buscador
390
- // parecía funcionar —salían productos— y no eran los buscados. No daba
391
- // ningún error; simplemente el buscador no buscaba.
392
- if (params.q) q.set("q", String(params.q))
393
- const r = await api(`/tienda/productos?${q}`)
394
- return {
395
- items: (r.productos ?? []).map(mapProductoPropio),
396
- count: r.total ?? 0,
397
- limit: params.limit ?? 24,
398
- offset: params.offset ?? 0,
399
- }
400
- }
401
- const r = await region()
402
200
  const limit = params.limit ?? 24
403
201
  const offset = params.offset ?? 0
404
-
405
- let categoryId = params.categoryId
406
- if (!categoryId && params.category) {
407
- const c = await getCategoryRaw(params.category)
408
- if (!c) return { items: [], count: 0, limit, offset }
409
- categoryId = c.id
410
- }
411
-
412
- const qs = new URLSearchParams({ limit: String(limit), offset: String(offset), fields: PRODUCT_FIELDS })
413
- if (r?.id) qs.set("region_id", r.id)
414
- if (params.q) qs.set("q", params.q)
415
- if (categoryId) qs.append("category_id[]", categoryId)
416
- for (const id of params.ids ?? []) qs.append("id[]", id)
417
- if (params.sort === "newest") qs.set("order", "-created_at")
418
-
419
- const { products = [], count = 0 } = await api(`/store/products?${qs}`)
420
- let items = products.map((p) => mapProduct(p, r?.currency_code))
421
-
422
- // Ordenar por precio en el cliente sería mentir: solo ordenaría la página
423
- // actual. Se delega en el endpoint del backend, y si no está, se avisa.
424
- if (params.sort === "price_asc" || params.sort === "price_desc") {
425
- const ordenados = await idsPorPrecio({ ...params, limit, offset, categoryId, region: r })
426
- // Se exige la forma esperada, no solo que la respuesta no sea nula: un
427
- // proxy mal configurado puede contestar 200 con cualquier otra cosa.
428
- if (Array.isArray(ordenados?.ids)) {
429
- const porId = new Map(items.map((p) => [p.id, p]))
430
- const faltantes = ordenados.ids.filter((id) => !porId.has(id))
431
- if (faltantes.length) {
432
- for (const p of await getProductsByIds(faltantes)) porId.set(p.id, p)
433
- }
434
- items = ordenados.ids.map((id) => porId.get(id)).filter(Boolean)
435
- return { items, count: ordenados.count, limit, offset }
436
- }
437
- }
438
-
439
- return { items, count, limit, offset }
440
- }
441
-
442
- /**
443
- * La Store API no sabe ordenar por precio (los precios viven en otro módulo y
444
- * `order=variants.calculated_price` devuelve 500). El backend expone
445
- * `/tienda/products-by-price`; si no está, se devuelve null y el llamador se
446
- * queda con el orden por defecto en vez de romper.
447
- */
448
- async function idsPorPrecio({ q, categoryId, sort, limit, offset, region: r }) {
449
- const qs = new URLSearchParams({
450
- limit: String(limit),
451
- offset: String(offset),
452
- order: sort === "price_desc" ? "desc" : "asc",
453
- })
454
- if (q) qs.set("q", q)
455
- if (categoryId) qs.set("category_id", categoryId)
456
- if (r?.currency_code) qs.set("currency_code", r.currency_code)
457
- try {
458
- return await api(`/tienda/products-by-price?${qs}`)
459
- } catch {
460
- return null
461
- }
202
+ const qs = new URLSearchParams({ limite: String(limit), desde: String(offset), moneda: await monedaDeLaTienda() })
203
+ if (params.q) qs.set("q", String(params.q))
204
+ if (params.category) qs.set("categoria_handle", params.category)
205
+ if (params.categoryId) qs.set("categoria", params.categoryId)
206
+ if (params.ids?.length) qs.set("ids", params.ids.join(","))
207
+ if (ORDEN_DEL_CONTRATO[params.sort]) qs.set("orden", ORDEN_DEL_CONTRATO[params.sort])
208
+ const r = await api(`/tienda/productos?${qs}`)
209
+ return { items: (r.productos ?? []).map(mapProducto), count: r.total ?? 0, limit, offset }
462
210
  }
463
211
 
464
212
  async function getProduct(handle) {
465
- if (catalogoPropio) {
466
- try {
467
- const r = await api(`/tienda/productos/${encodeURIComponent(handle)}`)
468
- return r.producto ? mapProductoPropio(r.producto) : null
469
- } catch (e) {
470
- // Un 404 aquí es «no existe», no un fallo: el escaparate lo trata como
471
- // página no encontrada, no como tienda caída.
472
- if (e?.status === 404) return null
473
- throw e
474
- }
213
+ try {
214
+ const r = await api(`/tienda/productos/${encodeURIComponent(handle)}`)
215
+ return r.producto ? mapProducto(r.producto) : null
216
+ } catch (e) {
217
+ if (e?.status === 404) return null
218
+ throw e
475
219
  }
476
- const r = await region()
477
- const qs = new URLSearchParams({ handle, limit: "1", fields: PRODUCT_FIELDS })
478
- if (r?.id) qs.set("region_id", r.id)
479
- const { products = [] } = await api(`/store/products?${qs}`)
480
- return products[0] ? mapProduct(products[0], r?.currency_code) : null
481
220
  }
482
221
 
483
222
  async function getProductsByIds(ids) {
484
223
  if (!ids?.length) return []
485
- const r = await region()
486
- const qs = new URLSearchParams({ limit: String(ids.length), fields: PRODUCT_FIELDS })
487
- if (r?.id) qs.set("region_id", r.id)
488
- for (const id of ids) qs.append("id[]", id)
489
- const { products = [] } = await api(`/store/products?${qs}`)
490
- return products.map((p) => mapProduct(p, r?.currency_code))
491
- }
492
-
493
- async function getCategoryRaw(handle) {
494
- if (catalogoPropio) {
495
- const { product_categories = [] } = await api(`/tienda/categorias?handle=${encodeURIComponent(handle)}`)
496
- return product_categories[0] ?? null
497
- }
498
- const qs = new URLSearchParams({ handle, limit: "1", fields: "id,name,handle,description,parent_category_id,metadata" })
499
- const { product_categories = [] } = await api(`/store/product-categories?${qs}`)
500
- return product_categories[0] ?? null
224
+ const { items } = await listProducts({ ids, limit: ids.length })
225
+ const porId = new Map(items.map((p) => [p.id, p]))
226
+ return ids.map((id) => porId.get(id)).filter(Boolean)
501
227
  }
502
228
 
503
229
  async function listCategories() {
504
- if (catalogoPropio) {
505
- const { product_categories = [] } = await api("/tienda/categorias")
506
- // 🔴 Solo las de primer nivel: vienen planas Y anidadas a la vez, así que
507
- // devolverlas todas duplicaría cada hija dentro y fuera de su madre, y un
508
- // menú saldría con las subcategorías repetidas al final.
509
- return product_categories.filter((c) => !c.parent_category_id).map(mapCategory)
510
- }
511
- const qs = new URLSearchParams({
512
- limit: "200",
513
- fields: "id,name,handle,description,parent_category_id,metadata,*category_children",
514
- })
515
- const { product_categories = [] } = await api(`/store/product-categories?${qs}`)
516
- return product_categories.map(mapCategory)
230
+ const { product_categories = [] } = await api("/tienda/categorias")
231
+ return product_categories.filter((c) => !c.parent_category_id).map(mapCategory)
517
232
  }
518
233
 
519
234
  async function getCategory(handle) {
520
- const c = await getCategoryRaw(handle)
521
- return c ? mapCategory(c) : null
235
+ const { product_categories = [] } = await api(`/tienda/categorias?handle=${encodeURIComponent(handle)}`)
236
+ return product_categories[0] ? mapCategory(product_categories[0]) : null
522
237
  }
523
238
 
524
- /** Pedido por id, para la página de confirmación. */
525
239
  async function getOrder(id) {
526
240
  try {
527
- const { order } = await api(`/store/orders/${id}`)
528
- return mapOrder(order)
241
+ const { order } = await api(`/tienda/pedidos/${encodeURIComponent(id)}`)
242
+ return order ?? null
529
243
  } catch (e) {
530
244
  if (e.status === 404 || e.status === 401) return null
531
245
  throw e
@@ -537,112 +251,131 @@ export function createCommerce({
537
251
  return items
538
252
  }
539
253
 
540
- // ── Carrito ─────────────────────────────────────────────────────────────
254
+ const rutaCarrito = (cartId, resto = "") => `/tienda/carritos/${encodeURIComponent(cartId)}${resto}`
541
255
 
542
256
  const getCart = async (id) => {
543
257
  try {
544
- const { cart } = await api(`/store/carts/${id}?fields=${encodeURIComponent(CART_FIELDS)}`)
258
+ const { cart } = await api(rutaCarrito(id))
545
259
  return mapCart(cart)
546
260
  } catch (e) {
547
- // Un carrito caducado o completado es un caso normal, no un fallo: el
548
- // tema debe crear uno nuevo, no enseñar una pantalla de error.
549
261
  if (e.status === 404) return null
550
262
  throw e
551
263
  }
552
264
  }
553
265
 
266
+ const subirFicheroCliente = async (archivo) => {
267
+ const cuerpo = new FormData()
268
+ cuerpo.append("fichero", archivo, archivo.name)
269
+ const r = await f(`${raiz}/tienda/personalizacion/ficheros`, {
270
+ method: "POST",
271
+ headers: publishableKey ? { "x-pcc-clave": publishableKey } : {},
272
+ body: cuerpo,
273
+ })
274
+ const datos = await r.json().catch(() => ({}))
275
+ if (!r.ok) throw new Error(datos?.message || "No se pudo subir el fichero.")
276
+ return datos.fichero
277
+ }
278
+
554
279
  const cart = {
555
280
  get: getCart,
556
281
  async create() {
557
282
  const r = await region()
558
- const { cart } = await api("/store/carts", { method: "POST", body: r?.id ? { region_id: r.id } : {} })
559
- return mapCart(cart)
283
+ const { cart: c } = await api("/tienda/carritos", {
284
+ method: "POST",
285
+ body: r?.id ? { region_id: r.id, moneda: r.currency_code } : {},
286
+ })
287
+ return mapCart(c)
560
288
  },
561
- async addItem(cartId, variantId, quantity) {
562
- const { cart } = await api(`/store/carts/${cartId}/line-items`, {
289
+ async addItem(cartId, variantId, quantity, personalizacion, pack) {
290
+ const { cart: c } = await api(rutaCarrito(cartId, "/lineas"), {
563
291
  method: "POST",
564
- body: { variant_id: variantId, quantity },
292
+ body: {
293
+ variant_id: variantId,
294
+ quantity,
295
+ ...(personalizacion ? { personalizacion } : {}),
296
+ ...(pack ? { pack } : {}),
297
+ },
565
298
  })
566
- return mapCart(cart)
299
+ return mapCart(c)
300
+ },
301
+ async consentirDigital(cartId, aceptado) {
302
+ const { cart: c } = await api(rutaCarrito(cartId, "/consentimiento-digital"), {
303
+ method: "POST",
304
+ body: { aceptado },
305
+ })
306
+ return mapCart(c)
567
307
  },
568
308
  async updateItem(cartId, lineId, quantity) {
569
309
  if (quantity <= 0) return cart.removeItem(cartId, lineId)
570
- const { cart: c } = await api(`/store/carts/${cartId}/line-items/${lineId}`, {
310
+ const { cart: c } = await api(rutaCarrito(cartId, `/lineas/${encodeURIComponent(lineId)}`), {
571
311
  method: "POST",
572
312
  body: { quantity },
573
313
  })
574
314
  return mapCart(c)
575
315
  },
576
316
  async removeItem(cartId, lineId) {
577
- await api(`/store/carts/${cartId}/line-items/${lineId}`, { method: "DELETE" })
578
- return getCart(cartId)
317
+ const { cart: c } = await api(rutaCarrito(cartId, `/lineas/${encodeURIComponent(lineId)}`), { method: "DELETE" })
318
+ return mapCart(c)
579
319
  },
580
320
  async applyPromo(cartId, code) {
581
- const actual = await getCart(cartId)
582
- const { cart } = await api(`/store/carts/${cartId}`, {
583
- method: "POST",
584
- body: { promo_codes: [...new Set([...(actual?.promoCodes ?? []), code])] },
585
- })
586
- return mapCart(cart)
321
+ const r = await api(rutaCarrito(cartId, "/cupon"), { method: "POST", body: { codigo: code } })
322
+ if (!r.aplicado) {
323
+ const err = new Error(r.motivo ?? "Ese código no se puede usar.")
324
+ err.status = 400
325
+ err.detalle = r.motivo ?? ""
326
+ throw err
327
+ }
328
+ return mapCart(r.cart)
587
329
  },
588
330
  async removePromo(cartId, code) {
589
- const actual = await getCart(cartId)
590
- const { cart } = await api(`/store/carts/${cartId}`, {
591
- method: "POST",
592
- body: { promo_codes: (actual?.promoCodes ?? []).filter((c) => c !== code) },
593
- })
594
- return mapCart(cart)
331
+ const { cart: c } = await api(rutaCarrito(cartId, `/cupon/${encodeURIComponent(code)}`), { method: "DELETE" })
332
+ return mapCart(c)
595
333
  },
596
334
  }
597
335
 
598
- // ── Checkout ────────────────────────────────────────────────────────────
599
-
600
336
  const checkout = {
337
+ async listCountries() {
338
+ const { regions = [] } = await api("/tienda/regiones")
339
+ return [...new Set(regions.flatMap((r) => (r.countries ?? []).map((c) => String(c.iso_2).toLowerCase())))].filter(Boolean)
340
+ },
341
+ async setVatNumber(cartId, vatNumber) {
342
+ const { cart: c } = await api(rutaCarrito(cartId, "/nif-iva"), {
343
+ method: "POST",
344
+ body: { vat_number: vatNumber ?? null },
345
+ })
346
+ return mapCart(c)
347
+ },
601
348
  async setEmail(cartId, email) {
602
- const { cart } = await api(`/store/carts/${cartId}`, { method: "POST", body: { email } })
603
- return mapCart(cart)
349
+ const { cart: c } = await api(rutaCarrito(cartId, "/datos"), { method: "POST", body: { email } })
350
+ return mapCart(c)
604
351
  },
605
352
  async setAddresses(cartId, shipping, billing) {
606
- const { cart } = await api(`/store/carts/${cartId}`, {
353
+ const { cart: c } = await api(rutaCarrito(cartId, "/datos"), {
607
354
  method: "POST",
608
355
  body: {
609
356
  shipping_address: aDireccionApi(shipping),
610
357
  billing_address: aDireccionApi(billing ?? shipping),
611
358
  },
612
359
  })
613
- return mapCart(cart)
360
+ return mapCart(c)
614
361
  },
615
362
  async listShippingOptions(cartId) {
616
- const { shipping_options = [] } = await api(`/store/shipping-options?cart_id=${cartId}`)
617
- return shipping_options.map((o) => ({
363
+ const { moneda = "eur", opciones = [] } = await api(rutaCarrito(cartId, "/envios"))
364
+ return opciones.map((o) => ({
618
365
  id: o.id,
619
- name: o.name,
620
- price: money(o.amount ?? o.calculated_price?.calculated_amount, o.calculated_price?.currency_code ?? "eur", true),
621
- priceCalculated: o.price_type === "calculated",
622
- description: o.data?.description ?? null,
366
+ name: o.nombre,
367
+ price: money(o.precio ?? 0, moneda, true),
368
+ priceCalculated: Boolean(o.esCalculada),
369
+ description: o.datos?.description ?? null,
623
370
  }))
624
371
  },
625
- /**
626
- * De cuántos sitios sale el carrito.
627
- *
628
- * Devuelve `null` si el backend no tiene marketplace montado: la ruta no
629
- * existe y responde 404. El tema lo interpreta como «una tienda normal» y
630
- * sigue con el selector de envío de siempre, sin enterarse de nada.
631
- */
632
372
  async listShippingGroups(cartId) {
633
373
  try {
634
- return await api(`/tienda/marketplace/envios?cart_id=${cartId}`)
374
+ return await api(`/tienda/marketplace/envios?cart_id=${encodeURIComponent(cartId)}`)
635
375
  } catch {
636
376
  return null
637
377
  }
638
378
  },
639
- /**
640
- * Los portes de todos los grupos, en UNA llamada.
641
- *
642
- * Mandarlos de uno en uno no vale: el segundo borra al primero.
643
- * Lo impone la ruta del backend, que rechaza un conjunto incompleto en vez
644
- * de cobrar de menos calladamente.
645
- */
646
379
  async setShippingMethods(cartId, elecciones) {
647
380
  await api("/tienda/marketplace/envios", {
648
381
  method: "POST",
@@ -653,102 +386,118 @@ export function createCommerce({
653
386
  })
654
387
  },
655
388
  async setShippingMethod(cartId, optionId) {
656
- const { cart } = await api(`/store/carts/${cartId}/shipping-methods`, {
389
+ const { cart: c } = await api(rutaCarrito(cartId, "/envios"), {
657
390
  method: "POST",
658
- body: { option_id: optionId },
391
+ body: { opcion_id: optionId },
659
392
  })
660
- return mapCart(cart)
393
+ return mapCart(c)
661
394
  },
662
395
  async listPaymentMethods(cartId) {
663
- const c = await getCart(cartId)
664
- const r = c?.regionId ?? (await region())?.id
665
- if (!r) return []
666
- const { payment_providers = [] } = await api(`/store/payment-providers?region_id=${r}`)
667
- // El recargo configurado en la tienda, para poder enseñarlo junto al
668
- // método. Si la ruta no existe (backend anterior), simplemente no hay.
669
- let cfg = {}
670
- try {
671
- const r2 = await api("/payment-config")
672
- cfg = r2?.methods ?? {}
673
- } catch {}
674
- return payment_providers.map((p) => ({
675
- id: p.id,
676
- provider: p.id,
677
- name: cfg[p.id]?.name || p.id,
678
- ...(cfg[p.id]?.surcharge ? { surcharge: Number(cfg[p.id].surcharge) } : {}),
396
+ const { metodos = [] } = await api(rutaCarrito(cartId, "/pagos"))
397
+ return metodos.map((m) => ({
398
+ id: m.id,
399
+ provider: m.provider ?? m.id,
400
+ name: m.name ?? m.id,
401
+ description: m.description ?? null,
402
+ instructions: m.instructions ?? null,
403
+ ...(m.surcharge ? { surcharge: Number(m.surcharge) } : {}),
679
404
  }))
680
405
  },
681
406
  async selectPaymentMethod(cartId, provider, datos) {
682
- // El recargo de la forma de pago, ANTES de crear el cobro: el importe a
683
- // cobrar sale del total del carrito. Al revés, el recargo entraría en el
684
- // pedido pero no en lo que se le cobra al cliente.
685
- //
686
- // Si el backend no tiene la ruta (versión anterior), no hay recargos y se
687
- // sigue igual que siempre.
688
- try {
689
- await api("/tienda/pagos/recargo", {
690
- method: "POST",
691
- body: { cart_id: cartId, provider_id: provider },
692
- })
693
- } catch {}
694
-
695
- const { payment_collection } = await api("/store/payment-collections", {
696
- method: "POST",
697
- body: { cart_id: cartId },
698
- })
699
- await api(`/store/payment-collections/${payment_collection.id}/payment-sessions`, {
407
+ const { cart: c } = await api(rutaCarrito(cartId, "/pago"), {
700
408
  method: "POST",
701
- // `data` lo lee el proveedor de pago del backend. Sirve para decirle
702
- // cosas que solo sabe el escaparate — por ejemplo a qué RUTA debe
703
- // volver el comprador, que cambia con el idioma de la página.
704
- body: { provider_id: provider, ...(datos ? { data: datos } : {}) },
409
+ body: { proveedor: provider, ...(datos ? { datos } : {}) },
705
410
  })
706
- return getCart(cartId)
411
+ return mapCart(c)
707
412
  },
708
413
  async complete(cartId) {
709
- const r = await api(`/store/carts/${cartId}/complete`, { method: "POST" })
710
- // El backend contesta con el pedido o, si algo falló al cobrar, con el carrito
711
- // intacto y el motivo. Las dos son respuestas 200: el tema tiene que
712
- // mirar el tipo, no el código HTTP.
713
- if (r?.type === "order" && r.order) return { order: mapOrder(r.order) }
714
- return { cart: mapCart(r?.cart), error: r?.error?.message ?? "No se pudo completar el pedido" }
414
+ try {
415
+ const r = await api(rutaCarrito(cartId, "/completar"), { method: "POST", body: {} })
416
+ const pedido = (await getOrder(r.order.id)) ?? {
417
+ id: r.order.id,
418
+ status: "pending",
419
+ items: [],
420
+ subtotal: money(0, r.order.currency_code ?? "eur", true),
421
+ shipping: money(0, r.order.currency_code ?? "eur", true),
422
+ discount: money(0, r.order.currency_code ?? "eur", true),
423
+ tax: money(0, r.order.currency_code ?? "eur", true),
424
+ total: money(r.order.total, r.order.currency_code ?? "eur", true),
425
+ }
426
+ return { order: pedido }
427
+ } catch (e) {
428
+ if (e.status !== 409 && e.status !== 400) throw e
429
+ return { cart: await getCart(cartId), error: e.detalle || "No se pudo completar el pedido" }
430
+ }
715
431
  },
716
432
  }
717
433
 
718
- // ── Cuenta y contenido ──────────────────────────────────────────────────
719
-
720
434
  const account = {
721
- async login(email, password) {
722
- const r = await api("/auth/customer/emailpass", { method: "POST", body: { email, password } })
435
+ async login(email, password, cartId) {
436
+ const r = await api("/tienda/cuenta/entrar", {
437
+ method: "POST",
438
+ body: { email, password, ...(cartId ? { cart_id: cartId } : {}) },
439
+ })
723
440
  return { token: r.token }
724
441
  },
725
442
  async register({ email, password, firstName, lastName }) {
726
- const { token } = await api("/auth/customer/emailpass/register", {
443
+ const r = await api("/tienda/cuenta/registrar", {
727
444
  method: "POST",
728
- body: { email, password },
445
+ body: { email, password, first_name: firstName, last_name: lastName },
729
446
  })
730
- const { customer } = await api("/store/customers", {
731
- method: "POST",
732
- token,
733
- body: { email, first_name: firstName, last_name: lastName },
734
- })
735
- return { id: customer.id, email: customer.email, firstName: customer.first_name, lastName: customer.last_name }
447
+ return { id: r.customer_id, email, firstName: firstName ?? null, lastName: lastName ?? null }
736
448
  },
737
449
  async me(token) {
738
450
  try {
739
- const { customer } = await api("/store/customers/me", { token })
740
- return { id: customer.id, email: customer.email, firstName: customer.first_name, lastName: customer.last_name, phone: customer.phone }
451
+ const { customer } = await api("/tienda/cuenta", { token })
452
+ return customer
453
+ ? {
454
+ id: customer.id,
455
+ email: customer.email,
456
+ firstName: customer.first_name ?? null,
457
+ lastName: customer.last_name ?? null,
458
+ phone: customer.phone ?? null,
459
+ }
460
+ : null
741
461
  } catch (e) {
742
462
  if (e.status === 401) return null
743
463
  throw e
744
464
  }
745
465
  },
746
466
  async orders(token) {
747
- const { orders = [] } = await api("/store/orders", { token })
748
- return orders.map(mapOrder)
467
+ const { orders = [] } = await api("/tienda/cuenta/pedidos", { token })
468
+ return orders
469
+ },
470
+ async confirmEmail(sessionToken, token) {
471
+ try {
472
+ const r = await api("/tienda/cuenta/confirmar", { method: "POST", token: sessionToken, body: { token } })
473
+ return { adopted: r.pedidos_recuperados ?? 0 }
474
+ } catch (e) {
475
+ if (e.status === 400 || e.status === 401) return null
476
+ throw e
477
+ }
749
478
  },
750
479
  }
751
480
 
481
+ const mapPost = (p) => ({
482
+ id: p.id,
483
+ handle: p.handle,
484
+ title: p.title,
485
+ excerpt: p.excerpt ?? null,
486
+ content: p.content ?? p.body ?? null,
487
+ image: p.image ?? p.thumbnail ?? null,
488
+ publishedAt: p.published_at ?? p.created_at ?? null,
489
+ tags: p.tags ?? [],
490
+ })
491
+
492
+ const mapPagina = (p) => ({
493
+ handle: p.handle,
494
+ title: p.titulo ?? p.title ?? p.handle,
495
+ content: p.contenido ?? p.content ?? null,
496
+ order: p.orden ?? p.order ?? 0,
497
+ seoTitle: p.seo_titulo ?? p.seoTitle ?? null,
498
+ seoDescription: p.seo_descripcion ?? p.seoDescription ?? null,
499
+ })
500
+
752
501
  const content = {
753
502
  async listPosts(limit = 10) {
754
503
  try {
@@ -768,11 +517,9 @@ export function createCommerce({
768
517
  },
769
518
  async listPages() {
770
519
  try {
771
- const { paginas = [] } = await api(`/tienda/paginas`)
520
+ const { paginas = [] } = await api("/tienda/paginas")
772
521
  return paginas.map(mapPagina)
773
522
  } catch {
774
- // Sin páginas el pie se pinta sin enlaces, que es peor que con ellos y
775
- // muchísimo mejor que una tienda que no carga.
776
523
  return []
777
524
  }
778
525
  },
@@ -781,33 +528,11 @@ export function createCommerce({
781
528
  const { pagina } = await api(`/tienda/paginas/${encodeURIComponent(handle)}`)
782
529
  return pagina ? mapPagina(pagina) : null
783
530
  } catch {
784
- // Un borrador o una que no existe dan 404, y eso es «no hay página»,
785
- // no un error de la tienda.
786
531
  return null
787
532
  }
788
533
  },
789
534
  }
790
535
 
791
- const mapPost = (p) => ({
792
- id: p.id,
793
- handle: p.handle,
794
- title: p.title,
795
- excerpt: p.excerpt ?? null,
796
- content: p.content ?? p.body ?? null,
797
- image: p.image ?? p.thumbnail ?? null,
798
- publishedAt: p.published_at ?? p.created_at ?? null,
799
- tags: p.tags ?? [],
800
- })
801
-
802
- const mapPagina = (p) => ({
803
- handle: p.handle,
804
- title: p.titulo ?? p.title ?? p.handle,
805
- content: p.contenido ?? p.content ?? null,
806
- order: p.orden ?? p.order ?? 0,
807
- seoTitle: p.seo_titulo ?? p.seoTitle ?? null,
808
- seoDescription: p.seo_descripcion ?? p.seoDescription ?? null,
809
- })
810
-
811
536
  return {
812
537
  adapter: "pcreative",
813
538
  listProducts,
@@ -817,9 +542,52 @@ export function createCommerce({
817
542
  getCategory,
818
543
  search,
819
544
  getOrder,
545
+ subirFichero: subirFicheroCliente,
546
+ async getWithdrawal(orderId) {
547
+ try {
548
+ const { desistimiento } = await api(`/tienda/pedidos/${encodeURIComponent(orderId)}/desistimiento`)
549
+ return desistimiento ?? null
550
+ } catch (e) {
551
+ if (e.status === 404) return null
552
+ throw e
553
+ }
554
+ },
555
+ async requestWithdrawal(orderId, datos = {}) {
556
+ return await api(`/tienda/pedidos/${encodeURIComponent(orderId)}/desistimiento`, { method: "POST", body: datos })
557
+ },
558
+ async getDownloads(orderId) {
559
+ try {
560
+ const { descargas } = await api(`/tienda/pedidos/${encodeURIComponent(orderId)}/descargas`)
561
+ return (descargas ?? []).map((d) => ({ ...d, url: d.url ? `${raiz}${d.url}` : null }))
562
+ } catch (e) {
563
+ if (e.status === 404) return []
564
+ throw e
565
+ }
566
+ },
820
567
  cart,
821
568
  checkout,
822
569
  account,
823
570
  content,
571
+ async getStore() {
572
+ try {
573
+ const { tienda, legal } = await api("/tienda/tienda")
574
+ if (!tienda) return null
575
+ return {
576
+ name: tienda.nombre ?? null,
577
+ legalName: tienda.razon_social ?? null,
578
+ taxId: tienda.nif ?? null,
579
+ address: tienda.direccion ?? null,
580
+ email: tienda.correo ?? null,
581
+ phone: tienda.telefono ?? null,
582
+ country: tienda.pais ?? null,
583
+ legal: legal ?? null,
584
+ }
585
+ } catch {
586
+ return null
587
+ }
588
+ },
589
+ async sendContact({ name, email, subject, message }) {
590
+ await api("/tienda/contact", { method: "POST", body: { name, email, subject, message } })
591
+ },
824
592
  }
825
593
  }