@pcreative/commerce-contract 1.0.0 → 1.3.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/package.json +1 -1
- package/src/adapters/api.d.ts +0 -24
- package/src/adapters/api.js +296 -526
- package/src/index.d.ts +161 -2
- package/src/index.js +0 -20
package/src/adapters/api.js
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
let regionPromesa = null
|
|
52
|
+
|
|
113
53
|
function region() {
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
363
|
-
|
|
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:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
486
|
-
const
|
|
487
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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
|
|
521
|
-
return
|
|
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(`/
|
|
528
|
-
return
|
|
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,133 @@ export function createCommerce({
|
|
|
537
251
|
return items
|
|
538
252
|
}
|
|
539
253
|
|
|
540
|
-
|
|
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(
|
|
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("/
|
|
559
|
-
|
|
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(
|
|
289
|
+
async addItem(cartId, variantId, quantity, personalizacion, pack) {
|
|
290
|
+
const { cart: c } = await api(rutaCarrito(cartId, "/lineas"), {
|
|
563
291
|
method: "POST",
|
|
564
|
-
body: {
|
|
292
|
+
body: {
|
|
293
|
+
variant_id: variantId,
|
|
294
|
+
quantity,
|
|
295
|
+
...(personalizacion ? { personalizacion } : {}),
|
|
296
|
+
...(pack ? { pack } : {}),
|
|
297
|
+
},
|
|
565
298
|
})
|
|
566
|
-
return mapCart(
|
|
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(`/
|
|
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(`/
|
|
578
|
-
return
|
|
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
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
590
|
-
|
|
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(
|
|
603
|
-
return mapCart(
|
|
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(
|
|
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(
|
|
360
|
+
return mapCart(c)
|
|
614
361
|
},
|
|
615
362
|
async listShippingOptions(cartId) {
|
|
616
|
-
const {
|
|
617
|
-
return
|
|
363
|
+
const { moneda = "eur", opciones = [] } = await api(rutaCarrito(cartId, "/envios"))
|
|
364
|
+
return opciones.map((o) => ({
|
|
618
365
|
id: o.id,
|
|
619
|
-
name: o.
|
|
620
|
-
price: money(o.
|
|
621
|
-
priceCalculated: o.
|
|
622
|
-
description: o.
|
|
366
|
+
name: o.nombre,
|
|
367
|
+
price: money(o.precio ?? 0, moneda, true),
|
|
368
|
+
priceCalculated: Boolean(o.esCalculada),
|
|
369
|
+
description: o.datos?.description ?? null,
|
|
370
|
+
deliveryTime: o.plazo ?? null,
|
|
371
|
+
pickup: Boolean(o.recogida),
|
|
623
372
|
}))
|
|
624
373
|
},
|
|
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
374
|
async listShippingGroups(cartId) {
|
|
633
375
|
try {
|
|
634
|
-
return await api(`/tienda/marketplace/envios?cart_id=${cartId}`)
|
|
376
|
+
return await api(`/tienda/marketplace/envios?cart_id=${encodeURIComponent(cartId)}`)
|
|
635
377
|
} catch {
|
|
636
378
|
return null
|
|
637
379
|
}
|
|
638
380
|
},
|
|
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
381
|
async setShippingMethods(cartId, elecciones) {
|
|
647
382
|
await api("/tienda/marketplace/envios", {
|
|
648
383
|
method: "POST",
|
|
@@ -653,102 +388,118 @@ export function createCommerce({
|
|
|
653
388
|
})
|
|
654
389
|
},
|
|
655
390
|
async setShippingMethod(cartId, optionId) {
|
|
656
|
-
const { cart } = await api(
|
|
391
|
+
const { cart: c } = await api(rutaCarrito(cartId, "/envios"), {
|
|
657
392
|
method: "POST",
|
|
658
|
-
body: {
|
|
393
|
+
body: { opcion_id: optionId },
|
|
659
394
|
})
|
|
660
|
-
return mapCart(
|
|
395
|
+
return mapCart(c)
|
|
661
396
|
},
|
|
662
397
|
async listPaymentMethods(cartId) {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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) } : {}),
|
|
398
|
+
const { metodos = [] } = await api(rutaCarrito(cartId, "/pagos"))
|
|
399
|
+
return metodos.map((m) => ({
|
|
400
|
+
id: m.id,
|
|
401
|
+
provider: m.provider ?? m.id,
|
|
402
|
+
name: m.name ?? m.id,
|
|
403
|
+
description: m.description ?? null,
|
|
404
|
+
instructions: m.instructions ?? null,
|
|
405
|
+
...(m.surcharge ? { surcharge: Number(m.surcharge) } : {}),
|
|
679
406
|
}))
|
|
680
407
|
},
|
|
681
408
|
async selectPaymentMethod(cartId, provider, datos) {
|
|
682
|
-
|
|
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`, {
|
|
409
|
+
const { cart: c } = await api(rutaCarrito(cartId, "/pago"), {
|
|
700
410
|
method: "POST",
|
|
701
|
-
|
|
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 } : {}) },
|
|
411
|
+
body: { proveedor: provider, ...(datos ? { datos } : {}) },
|
|
705
412
|
})
|
|
706
|
-
return
|
|
413
|
+
return mapCart(c)
|
|
707
414
|
},
|
|
708
415
|
async complete(cartId) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
416
|
+
try {
|
|
417
|
+
const r = await api(rutaCarrito(cartId, "/completar"), { method: "POST", body: {} })
|
|
418
|
+
const pedido = (await getOrder(r.order.id)) ?? {
|
|
419
|
+
id: r.order.id,
|
|
420
|
+
status: "pending",
|
|
421
|
+
items: [],
|
|
422
|
+
subtotal: money(0, r.order.currency_code ?? "eur", true),
|
|
423
|
+
shipping: money(0, r.order.currency_code ?? "eur", true),
|
|
424
|
+
discount: money(0, r.order.currency_code ?? "eur", true),
|
|
425
|
+
tax: money(0, r.order.currency_code ?? "eur", true),
|
|
426
|
+
total: money(r.order.total, r.order.currency_code ?? "eur", true),
|
|
427
|
+
}
|
|
428
|
+
return { order: pedido }
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (e.status !== 409 && e.status !== 400) throw e
|
|
431
|
+
return { cart: await getCart(cartId), error: e.detalle || "No se pudo completar el pedido" }
|
|
432
|
+
}
|
|
715
433
|
},
|
|
716
434
|
}
|
|
717
435
|
|
|
718
|
-
// ── Cuenta y contenido ──────────────────────────────────────────────────
|
|
719
|
-
|
|
720
436
|
const account = {
|
|
721
|
-
async login(email, password) {
|
|
722
|
-
const r = await api("/
|
|
437
|
+
async login(email, password, cartId) {
|
|
438
|
+
const r = await api("/tienda/cuenta/entrar", {
|
|
439
|
+
method: "POST",
|
|
440
|
+
body: { email, password, ...(cartId ? { cart_id: cartId } : {}) },
|
|
441
|
+
})
|
|
723
442
|
return { token: r.token }
|
|
724
443
|
},
|
|
725
444
|
async register({ email, password, firstName, lastName }) {
|
|
726
|
-
const
|
|
445
|
+
const r = await api("/tienda/cuenta/registrar", {
|
|
727
446
|
method: "POST",
|
|
728
|
-
body: { email, password },
|
|
447
|
+
body: { email, password, first_name: firstName, last_name: lastName },
|
|
729
448
|
})
|
|
730
|
-
|
|
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 }
|
|
449
|
+
return { id: r.customer_id, email, firstName: firstName ?? null, lastName: lastName ?? null }
|
|
736
450
|
},
|
|
737
451
|
async me(token) {
|
|
738
452
|
try {
|
|
739
|
-
const { customer } = await api("/
|
|
740
|
-
return
|
|
453
|
+
const { customer } = await api("/tienda/cuenta", { token })
|
|
454
|
+
return customer
|
|
455
|
+
? {
|
|
456
|
+
id: customer.id,
|
|
457
|
+
email: customer.email,
|
|
458
|
+
firstName: customer.first_name ?? null,
|
|
459
|
+
lastName: customer.last_name ?? null,
|
|
460
|
+
phone: customer.phone ?? null,
|
|
461
|
+
}
|
|
462
|
+
: null
|
|
741
463
|
} catch (e) {
|
|
742
464
|
if (e.status === 401) return null
|
|
743
465
|
throw e
|
|
744
466
|
}
|
|
745
467
|
},
|
|
746
468
|
async orders(token) {
|
|
747
|
-
const { orders = [] } = await api("/
|
|
748
|
-
return orders
|
|
469
|
+
const { orders = [] } = await api("/tienda/cuenta/pedidos", { token })
|
|
470
|
+
return orders
|
|
471
|
+
},
|
|
472
|
+
async confirmEmail(sessionToken, token) {
|
|
473
|
+
try {
|
|
474
|
+
const r = await api("/tienda/cuenta/confirmar", { method: "POST", token: sessionToken, body: { token } })
|
|
475
|
+
return { adopted: r.pedidos_recuperados ?? 0 }
|
|
476
|
+
} catch (e) {
|
|
477
|
+
if (e.status === 400 || e.status === 401) return null
|
|
478
|
+
throw e
|
|
479
|
+
}
|
|
749
480
|
},
|
|
750
481
|
}
|
|
751
482
|
|
|
483
|
+
const mapPost = (p) => ({
|
|
484
|
+
id: p.id,
|
|
485
|
+
handle: p.handle,
|
|
486
|
+
title: p.title,
|
|
487
|
+
excerpt: p.excerpt ?? null,
|
|
488
|
+
content: p.content ?? p.body ?? null,
|
|
489
|
+
image: p.image ?? p.thumbnail ?? null,
|
|
490
|
+
publishedAt: p.published_at ?? p.created_at ?? null,
|
|
491
|
+
tags: p.tags ?? [],
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
const mapPagina = (p) => ({
|
|
495
|
+
handle: p.handle,
|
|
496
|
+
title: p.titulo ?? p.title ?? p.handle,
|
|
497
|
+
content: p.contenido ?? p.content ?? null,
|
|
498
|
+
order: p.orden ?? p.order ?? 0,
|
|
499
|
+
seoTitle: p.seo_titulo ?? p.seoTitle ?? null,
|
|
500
|
+
seoDescription: p.seo_descripcion ?? p.seoDescription ?? null,
|
|
501
|
+
})
|
|
502
|
+
|
|
752
503
|
const content = {
|
|
753
504
|
async listPosts(limit = 10) {
|
|
754
505
|
try {
|
|
@@ -768,11 +519,9 @@ export function createCommerce({
|
|
|
768
519
|
},
|
|
769
520
|
async listPages() {
|
|
770
521
|
try {
|
|
771
|
-
const { paginas = [] } = await api(
|
|
522
|
+
const { paginas = [] } = await api("/tienda/paginas")
|
|
772
523
|
return paginas.map(mapPagina)
|
|
773
524
|
} 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
525
|
return []
|
|
777
526
|
}
|
|
778
527
|
},
|
|
@@ -781,33 +530,11 @@ export function createCommerce({
|
|
|
781
530
|
const { pagina } = await api(`/tienda/paginas/${encodeURIComponent(handle)}`)
|
|
782
531
|
return pagina ? mapPagina(pagina) : null
|
|
783
532
|
} 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
533
|
return null
|
|
787
534
|
}
|
|
788
535
|
},
|
|
789
536
|
}
|
|
790
537
|
|
|
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
538
|
return {
|
|
812
539
|
adapter: "pcreative",
|
|
813
540
|
listProducts,
|
|
@@ -817,9 +544,52 @@ export function createCommerce({
|
|
|
817
544
|
getCategory,
|
|
818
545
|
search,
|
|
819
546
|
getOrder,
|
|
547
|
+
subirFichero: subirFicheroCliente,
|
|
548
|
+
async getWithdrawal(orderId) {
|
|
549
|
+
try {
|
|
550
|
+
const { desistimiento } = await api(`/tienda/pedidos/${encodeURIComponent(orderId)}/desistimiento`)
|
|
551
|
+
return desistimiento ?? null
|
|
552
|
+
} catch (e) {
|
|
553
|
+
if (e.status === 404) return null
|
|
554
|
+
throw e
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
async requestWithdrawal(orderId, datos = {}) {
|
|
558
|
+
return await api(`/tienda/pedidos/${encodeURIComponent(orderId)}/desistimiento`, { method: "POST", body: datos })
|
|
559
|
+
},
|
|
560
|
+
async getDownloads(orderId) {
|
|
561
|
+
try {
|
|
562
|
+
const { descargas } = await api(`/tienda/pedidos/${encodeURIComponent(orderId)}/descargas`)
|
|
563
|
+
return (descargas ?? []).map((d) => ({ ...d, url: d.url ? `${raiz}${d.url}` : null }))
|
|
564
|
+
} catch (e) {
|
|
565
|
+
if (e.status === 404) return []
|
|
566
|
+
throw e
|
|
567
|
+
}
|
|
568
|
+
},
|
|
820
569
|
cart,
|
|
821
570
|
checkout,
|
|
822
571
|
account,
|
|
823
572
|
content,
|
|
573
|
+
async getStore() {
|
|
574
|
+
try {
|
|
575
|
+
const { tienda, legal } = await api("/tienda/tienda")
|
|
576
|
+
if (!tienda) return null
|
|
577
|
+
return {
|
|
578
|
+
name: tienda.nombre ?? null,
|
|
579
|
+
legalName: tienda.razon_social ?? null,
|
|
580
|
+
taxId: tienda.nif ?? null,
|
|
581
|
+
address: tienda.direccion ?? null,
|
|
582
|
+
email: tienda.correo ?? null,
|
|
583
|
+
phone: tienda.telefono ?? null,
|
|
584
|
+
country: tienda.pais ?? null,
|
|
585
|
+
legal: legal ?? null,
|
|
586
|
+
}
|
|
587
|
+
} catch {
|
|
588
|
+
return null
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
async sendContact({ name, email, subject, message }) {
|
|
592
|
+
await api("/tienda/contact", { method: "POST", body: { name, email, subject, message } })
|
|
593
|
+
},
|
|
824
594
|
}
|
|
825
595
|
}
|