@pcreative/commerce-contract 1.0.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/LICENSE +23 -0
- package/README.md +66 -0
- package/package.json +37 -0
- package/src/adapters/api.d.ts +36 -0
- package/src/adapters/api.js +825 -0
- package/src/index.d.ts +397 -0
- package/src/index.js +80 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contrato de datos de commerce (v1.0).
|
|
3
|
+
*
|
|
4
|
+
* Un tema NO importa el SDK del backend. Consume estas entidades y llama a este
|
|
5
|
+
* cliente; el adaptador traduce. De ahí salen las dos propiedades que sostienen
|
|
6
|
+
* el producto: el mismo tema sirve para otro backend mañana, y el backend no
|
|
7
|
+
* queda casado con temas de un solo framework.
|
|
8
|
+
*
|
|
9
|
+
* Los importes son SIEMPRE decimales en unidades mayores (19.99, no 1999) y
|
|
10
|
+
* llevan su moneda al lado, para que ningún tema tenga que adivinar la escala.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ── Entidades ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface Money {
|
|
16
|
+
/** Importe en unidades mayores: 19.99 €. */
|
|
17
|
+
amount: number
|
|
18
|
+
/** ISO 4217 en minúsculas: "eur". */
|
|
19
|
+
currency: string
|
|
20
|
+
/** true si `amount` ya lleva impuestos incluidos. */
|
|
21
|
+
taxIncluded?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Image {
|
|
25
|
+
url: string
|
|
26
|
+
alt?: string
|
|
27
|
+
width?: number
|
|
28
|
+
height?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface OptionValue {
|
|
32
|
+
/** Nombre de la opción: "Talla", "Color". */
|
|
33
|
+
name: string
|
|
34
|
+
value: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Variant {
|
|
38
|
+
id: string
|
|
39
|
+
title: string
|
|
40
|
+
sku?: string
|
|
41
|
+
barcode?: string
|
|
42
|
+
price: Money
|
|
43
|
+
/** Precio anterior si hay rebaja. */
|
|
44
|
+
compareAt?: Money | null
|
|
45
|
+
/** null = el backend no lleva stock de esta variante. */
|
|
46
|
+
stock: number | null
|
|
47
|
+
available: boolean
|
|
48
|
+
options: OptionValue[]
|
|
49
|
+
weight?: number | null
|
|
50
|
+
image?: Image | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Product {
|
|
54
|
+
id: string
|
|
55
|
+
handle: string
|
|
56
|
+
title: string
|
|
57
|
+
subtitle?: string | null
|
|
58
|
+
description?: string | null
|
|
59
|
+
brand?: string | null
|
|
60
|
+
/**
|
|
61
|
+
* Quién vende este producto en un marketplace.
|
|
62
|
+
*
|
|
63
|
+
* AUSENTE significa que lo vende la propia tienda, que es el caso de
|
|
64
|
+
* cualquier instalación sin marketplace. Opcional a propósito: un tema escrito
|
|
65
|
+
* antes de que esto existiera sigue funcionando sin tocar una línea.
|
|
66
|
+
*/
|
|
67
|
+
seller?: { id: string; name: string; slug: string } | null
|
|
68
|
+
images: Image[]
|
|
69
|
+
thumbnail?: string | null
|
|
70
|
+
/** Precio mínimo entre las variantes: el «desde X €» de la tarjeta. */
|
|
71
|
+
price: Money
|
|
72
|
+
variants: Variant[]
|
|
73
|
+
categories: CategoryRef[]
|
|
74
|
+
tags?: string[]
|
|
75
|
+
rating?: number | null
|
|
76
|
+
reviewCount?: number
|
|
77
|
+
available: boolean
|
|
78
|
+
metadata?: Record<string, unknown>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CategoryRef {
|
|
82
|
+
id: string
|
|
83
|
+
handle: string
|
|
84
|
+
name: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface Category extends CategoryRef {
|
|
88
|
+
description?: string | null
|
|
89
|
+
parentId?: string | null
|
|
90
|
+
children?: Category[]
|
|
91
|
+
image?: Image | null
|
|
92
|
+
productCount?: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Address {
|
|
96
|
+
firstName?: string
|
|
97
|
+
lastName?: string
|
|
98
|
+
company?: string
|
|
99
|
+
address1?: string
|
|
100
|
+
address2?: string
|
|
101
|
+
city?: string
|
|
102
|
+
province?: string
|
|
103
|
+
postalCode?: string
|
|
104
|
+
countryCode?: string
|
|
105
|
+
phone?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface LineItem {
|
|
109
|
+
id: string
|
|
110
|
+
productId: string
|
|
111
|
+
variantId: string
|
|
112
|
+
handle?: string
|
|
113
|
+
title: string
|
|
114
|
+
variantTitle?: string
|
|
115
|
+
thumbnail?: string | null
|
|
116
|
+
quantity: number
|
|
117
|
+
unitPrice: Money
|
|
118
|
+
total: Money
|
|
119
|
+
/**
|
|
120
|
+
* Es un CARGO, no mercancía: recargo por forma de pago y parecidos. No se
|
|
121
|
+
* envía y no tiene variante. Un tema que lo enseñe aparte debe excluirlo del
|
|
122
|
+
* subtotal, o lo cuenta dos veces.
|
|
123
|
+
*/
|
|
124
|
+
isFee?: boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Un sitio del que sale mercancía: un paquete, un porte. */
|
|
128
|
+
export interface ShippingGroup {
|
|
129
|
+
/** Identificador opaco. El tema lo devuelve tal cual al elegir. */
|
|
130
|
+
grupo: string
|
|
131
|
+
/** Nombre del vendedor que lo manda. Vacío = lo manda la tienda. */
|
|
132
|
+
vendedor?: string | null
|
|
133
|
+
vendedor_id?: string | null
|
|
134
|
+
lineas: { id: string; titulo: string; cantidad: number }[]
|
|
135
|
+
/**
|
|
136
|
+
* Lo que puede elegir ESTE paquete.
|
|
137
|
+
*
|
|
138
|
+
* Va por grupo y no en común porque cada vendedor puede tener sus propias
|
|
139
|
+
* tarifas, y porque un precio calculado (por peso) depende de lo que lleva
|
|
140
|
+
* ese paquete, no el carrito entero.
|
|
141
|
+
*
|
|
142
|
+
* `precio` en unidades mayores; vacío = todavía no se sabe (tarifa calculada
|
|
143
|
+
* que el transportista no ha resuelto). El tema debe decirlo, no inventarlo.
|
|
144
|
+
*/
|
|
145
|
+
opciones: { id: string; nombre: string; precio: number | null; calculado: boolean }[]
|
|
146
|
+
/** Lo ya elegido, para pintar la selección al recargar. */
|
|
147
|
+
option_id?: string | null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ShippingGroups {
|
|
151
|
+
grupos: ShippingGroup[]
|
|
152
|
+
moneda: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface EleccionEnvio {
|
|
156
|
+
grupo: string
|
|
157
|
+
optionId: string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ShippingOption {
|
|
161
|
+
id: string
|
|
162
|
+
name: string
|
|
163
|
+
price: Money
|
|
164
|
+
/** El backend calcula el precio al elegirla (envío por peso, por ejemplo). */
|
|
165
|
+
priceCalculated?: boolean
|
|
166
|
+
description?: string | null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface PaymentMethod {
|
|
170
|
+
id: string
|
|
171
|
+
/** Identificador del proveedor: "transfer", "cod", "bizum", "card"… */
|
|
172
|
+
provider: string
|
|
173
|
+
name?: string
|
|
174
|
+
/**
|
|
175
|
+
* Recargo de esta forma de pago, en unidades mayores. Sin definir = ninguno.
|
|
176
|
+
*
|
|
177
|
+
* Es para ENSEÑARLO. Quien lo cobra es el backend al elegir la forma de pago,
|
|
178
|
+
* leyéndolo de la configuración de la tienda: si viniera del escaparate,
|
|
179
|
+
* cualquiera podría pedir un recargo de cero.
|
|
180
|
+
*/
|
|
181
|
+
surcharge?: number
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Cómo sigue el comprador para pagar.
|
|
186
|
+
*
|
|
187
|
+
* La misma forma que usa `@pcreative/payments-contract`, escrita otra vez a
|
|
188
|
+
* propósito: este paquete no depende de nada, y atarlo al de pagos obligaría a
|
|
189
|
+
* instalarlo para pintar un catálogo.
|
|
190
|
+
*/
|
|
191
|
+
export type SiguientePago =
|
|
192
|
+
/** Mandar el navegador a la pasarela. */
|
|
193
|
+
| { tipo: "redirigir"; url: string; proveedor?: string }
|
|
194
|
+
/** Confirmar en el navegador con el SDK de la pasarela. `datos` es opaco. */
|
|
195
|
+
| {
|
|
196
|
+
tipo: "confirmar_en_cliente"
|
|
197
|
+
pasarela: string
|
|
198
|
+
datos: Record<string, unknown>
|
|
199
|
+
proveedor?: string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface Cart {
|
|
203
|
+
id: string
|
|
204
|
+
items: LineItem[]
|
|
205
|
+
subtotal: Money
|
|
206
|
+
shipping: Money
|
|
207
|
+
discount: Money
|
|
208
|
+
tax: Money
|
|
209
|
+
total: Money
|
|
210
|
+
itemCount: number
|
|
211
|
+
email?: string | null
|
|
212
|
+
shippingAddress?: Address | null
|
|
213
|
+
billingAddress?: Address | null
|
|
214
|
+
shippingMethods: { id: string; name: string; price: Money }[]
|
|
215
|
+
regionId?: string
|
|
216
|
+
promoCodes?: string[]
|
|
217
|
+
/**
|
|
218
|
+
* El paso que falta para pagar, si el método elegido lo pide. `null` con los
|
|
219
|
+
* métodos sin pasarela: ahí pagar es confirmar el pedido y ya está.
|
|
220
|
+
*/
|
|
221
|
+
pago?: SiguientePago | null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface Order {
|
|
225
|
+
id: string
|
|
226
|
+
displayId?: number
|
|
227
|
+
email?: string
|
|
228
|
+
status: string
|
|
229
|
+
items: LineItem[]
|
|
230
|
+
subtotal: Money
|
|
231
|
+
shipping: Money
|
|
232
|
+
discount: Money
|
|
233
|
+
tax: Money
|
|
234
|
+
total: Money
|
|
235
|
+
shippingAddress?: Address | null
|
|
236
|
+
createdAt?: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface Customer {
|
|
240
|
+
id: string
|
|
241
|
+
email: string
|
|
242
|
+
firstName?: string | null
|
|
243
|
+
lastName?: string | null
|
|
244
|
+
phone?: string | null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface Post {
|
|
248
|
+
id: string
|
|
249
|
+
handle: string
|
|
250
|
+
title: string
|
|
251
|
+
excerpt?: string | null
|
|
252
|
+
content?: string | null
|
|
253
|
+
image?: string | null
|
|
254
|
+
publishedAt?: string | null
|
|
255
|
+
tags?: string[]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Una página suelta del escaparate: «sobre nosotros», el aviso legal, los
|
|
260
|
+
* envíos. La escribe quien lleva la tienda desde el panel.
|
|
261
|
+
*
|
|
262
|
+
* 🔴 NO es una entrada de blog aunque se le parezca. Una entrada tiene fecha y
|
|
263
|
+
* se ordena por ella; una página tiene un ORDEN que decide una persona, porque
|
|
264
|
+
* lo que se pinta con ellas es el pie. Meterlas en el mismo tipo obligaría a
|
|
265
|
+
* inventarle una fecha al aviso legal.
|
|
266
|
+
*/
|
|
267
|
+
export interface PaginaSuelta {
|
|
268
|
+
handle: string
|
|
269
|
+
title: string
|
|
270
|
+
/** Solo al pedir UNA. El listado no lo trae: el pie no lo necesita. */
|
|
271
|
+
content?: string | null
|
|
272
|
+
/** Lo decide quien la escribe. Es el orden del pie. */
|
|
273
|
+
order?: number
|
|
274
|
+
seoTitle?: string | null
|
|
275
|
+
seoDescription?: string | null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Parámetros ────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
export type SortProductos = "relevance" | "price_asc" | "price_desc" | "newest" | "rating"
|
|
281
|
+
|
|
282
|
+
export interface ListProductsParams {
|
|
283
|
+
q?: string
|
|
284
|
+
/** Handle de categoría. */
|
|
285
|
+
category?: string
|
|
286
|
+
categoryId?: string
|
|
287
|
+
ids?: string[]
|
|
288
|
+
tags?: string[]
|
|
289
|
+
limit?: number
|
|
290
|
+
offset?: number
|
|
291
|
+
sort?: SortProductos
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface Page<T> {
|
|
295
|
+
items: T[]
|
|
296
|
+
/** Total de resultados, no de esta página. */
|
|
297
|
+
count: number
|
|
298
|
+
limit: number
|
|
299
|
+
offset: number
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── El cliente ────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
export interface CommerceClient {
|
|
305
|
+
/** Identificador del adaptador: "pcreative", "static"… */
|
|
306
|
+
readonly adapter: string
|
|
307
|
+
|
|
308
|
+
listProducts(params?: ListProductsParams): Promise<Page<Product>>
|
|
309
|
+
getProduct(handle: string): Promise<Product | null>
|
|
310
|
+
getProductsByIds(ids: string[]): Promise<Product[]>
|
|
311
|
+
listCategories(): Promise<Category[]>
|
|
312
|
+
getCategory(handle: string): Promise<Category | null>
|
|
313
|
+
search(q: string, limit?: number): Promise<Product[]>
|
|
314
|
+
/** Pedido por id, para la página de confirmación tras el checkout. */
|
|
315
|
+
getOrder(id: string): Promise<Order | null>
|
|
316
|
+
|
|
317
|
+
cart: {
|
|
318
|
+
get(id: string): Promise<Cart | null>
|
|
319
|
+
create(): Promise<Cart>
|
|
320
|
+
addItem(cartId: string, variantId: string, quantity: number): Promise<Cart>
|
|
321
|
+
updateItem(cartId: string, lineId: string, quantity: number): Promise<Cart>
|
|
322
|
+
removeItem(cartId: string, lineId: string): Promise<Cart>
|
|
323
|
+
applyPromo(cartId: string, code: string): Promise<Cart>
|
|
324
|
+
removePromo(cartId: string, code: string): Promise<Cart>
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
checkout: {
|
|
328
|
+
setEmail(cartId: string, email: string): Promise<Cart>
|
|
329
|
+
setAddresses(cartId: string, shipping: Address, billing?: Address): Promise<Cart>
|
|
330
|
+
listShippingOptions(cartId: string): Promise<ShippingOption[]>
|
|
331
|
+
setShippingMethod(cartId: string, optionId: string): Promise<Cart>
|
|
332
|
+
/**
|
|
333
|
+
* Opcional: de cuántos sitios sale el carrito.
|
|
334
|
+
*
|
|
335
|
+
* Una tienda normal devuelve un grupo y el tema puede ignorar todo esto y
|
|
336
|
+
* seguir con `listShippingOptions` / `setShippingMethod`. Un marketplace con
|
|
337
|
+
* vendedores que mandan lo suyo devuelve uno por sitio, y ahí un porte único
|
|
338
|
+
* sería cobrar un paquete y enviar tres.
|
|
339
|
+
*
|
|
340
|
+
* Un adaptador que no lo implemente no tiene marketplace: el tema lo
|
|
341
|
+
* comprueba con `typeof` y sigue por el camino de siempre.
|
|
342
|
+
*/
|
|
343
|
+
listShippingGroups?(cartId: string): Promise<ShippingGroups>
|
|
344
|
+
/**
|
|
345
|
+
* Fija los portes de TODOS los grupos a la vez.
|
|
346
|
+
*
|
|
347
|
+
* El conjunto entero, siempre. No es capricho de la firma: en el backend,
|
|
348
|
+
* mandar un porte suelto borra los demás sin avisar de nada. Un método
|
|
349
|
+
* «cambia el porte del grupo X» dejaría el carrito con uno solo.
|
|
350
|
+
*/
|
|
351
|
+
setShippingMethods?(cartId: string, elecciones: EleccionEnvio[]): Promise<void>
|
|
352
|
+
listPaymentMethods(cartId: string): Promise<PaymentMethod[]>
|
|
353
|
+
/**
|
|
354
|
+
* @param datos Lo que el escaparate quiera pasarle al proveedor de pago del
|
|
355
|
+
* backend. Se usa para la ruta de vuelta, que depende del idioma.
|
|
356
|
+
*/
|
|
357
|
+
selectPaymentMethod(cartId: string, provider: string, datos?: Record<string, unknown>): Promise<Cart>
|
|
358
|
+
complete(cartId: string): Promise<{ order: Order } | { cart: Cart; error: string }>
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Opcional: un adaptador puede no soportar cuentas de cliente. */
|
|
362
|
+
account?: {
|
|
363
|
+
login(email: string, password: string): Promise<{ token: string }>
|
|
364
|
+
register(datos: { email: string; password: string; firstName?: string; lastName?: string }): Promise<Customer>
|
|
365
|
+
me(token: string): Promise<Customer | null>
|
|
366
|
+
orders(token: string): Promise<Order[]>
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Opcional: contenido editorial servido por el backend. */
|
|
370
|
+
content?: {
|
|
371
|
+
listPosts(limit?: number): Promise<Post[]>
|
|
372
|
+
getPost(handle: string): Promise<Post | null>
|
|
373
|
+
/**
|
|
374
|
+
* Las páginas publicadas, SIN su contenido: es lo que pinta el pie, y
|
|
375
|
+
* traerse el texto de ocho páginas para enseñar ocho enlaces es tráfico
|
|
376
|
+
* tirado en cada visita.
|
|
377
|
+
*/
|
|
378
|
+
listPages?(): Promise<PaginaSuelta[]>
|
|
379
|
+
getPage?(handle: string): Promise<PaginaSuelta | null>
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── API ───────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
export declare const CONTRACT_VERSION: string
|
|
386
|
+
|
|
387
|
+
/** Formatea un importe con la moneda que lleva dentro. */
|
|
388
|
+
export declare function formatMoney(money: Money, locale?: string): string
|
|
389
|
+
|
|
390
|
+
/** Suma importes comprobando que son de la misma moneda. */
|
|
391
|
+
export declare function sumMoney(...importes: Money[]): Money
|
|
392
|
+
|
|
393
|
+
/** Construye un Money, con 0 como valor por defecto. */
|
|
394
|
+
export declare function money(amount: number | null | undefined, currency: string, taxIncluded?: boolean): Money
|
|
395
|
+
|
|
396
|
+
/** Comprueba que un objeto implementa el contrato; devuelve lo que falta. */
|
|
397
|
+
export declare function assertCommerceClient(cliente: unknown): string[]
|
package/src/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pcreative/commerce-contract — el contrato de datos que consume un tema.
|
|
3
|
+
*
|
|
4
|
+
* Aquí solo hay las utilidades comunes y el comprobador de conformidad: las
|
|
5
|
+
* entidades viven en `index.d.ts` (son forma, no código) y cada adaptador
|
|
6
|
+
* traduce lo suyo. Sin dependencias, para que valga igual en Next, Astro, Nuxt
|
|
7
|
+
* o un runtime edge.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const CONTRACT_VERSION = "1.0"
|
|
11
|
+
|
|
12
|
+
/** Construye un importe. Los nulos del backend se vuelven 0, no NaN. */
|
|
13
|
+
export function money(amount, currency, taxIncluded) {
|
|
14
|
+
const m = { amount: Number(amount ?? 0) || 0, currency: String(currency ?? "eur").toLowerCase() }
|
|
15
|
+
if (taxIncluded !== undefined) m.taxIncluded = taxIncluded
|
|
16
|
+
return m
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Formatea con `Intl`. Los importes del contrato son unidades mayores, así que
|
|
21
|
+
* no hay que dividir por 100 en ningún sitio — que es de donde salen la mitad
|
|
22
|
+
* de los errores de precio en las tiendas.
|
|
23
|
+
*/
|
|
24
|
+
export function formatMoney(m, locale = "es-ES") {
|
|
25
|
+
if (!m) return ""
|
|
26
|
+
try {
|
|
27
|
+
return new Intl.NumberFormat(locale, {
|
|
28
|
+
style: "currency",
|
|
29
|
+
currency: (m.currency ?? "eur").toUpperCase(),
|
|
30
|
+
}).format(m.amount ?? 0)
|
|
31
|
+
} catch {
|
|
32
|
+
return `${(m.amount ?? 0).toFixed(2)} ${(m.currency ?? "").toUpperCase()}`
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Suma importes. Mezclar monedas es un error, no algo que redondear. */
|
|
37
|
+
export function sumMoney(...importes) {
|
|
38
|
+
const validos = importes.filter(Boolean)
|
|
39
|
+
if (validos.length === 0) return money(0, "eur")
|
|
40
|
+
const currency = validos[0].currency
|
|
41
|
+
for (const m of validos) {
|
|
42
|
+
if (m.currency !== currency) {
|
|
43
|
+
throw new Error(`no se pueden sumar importes en ${currency} y en ${m.currency}`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return money(
|
|
47
|
+
validos.reduce((n, m) => n + (m.amount ?? 0), 0),
|
|
48
|
+
currency,
|
|
49
|
+
validos[0].taxIncluded
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const REQUERIDO = {
|
|
54
|
+
raiz: ["listProducts", "getProduct", "getProductsByIds", "listCategories", "getCategory", "search", "getOrder"],
|
|
55
|
+
cart: ["get", "create", "addItem", "updateItem", "removeItem"],
|
|
56
|
+
checkout: ["setEmail", "setAddresses", "listShippingOptions", "setShippingMethod", "listPaymentMethods", "selectPaymentMethod", "complete"],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Comprueba que un adaptador implementa el contrato. Devuelve la lista de lo
|
|
61
|
+
* que falta (vacía si cumple). Un tema puede llamarlo en desarrollo para
|
|
62
|
+
* enterarse al arrancar y no en mitad del checkout.
|
|
63
|
+
*/
|
|
64
|
+
export function assertCommerceClient(cliente) {
|
|
65
|
+
const faltan = []
|
|
66
|
+
if (!cliente || typeof cliente !== "object") return ["el cliente no es un objeto"]
|
|
67
|
+
for (const m of REQUERIDO.raiz) {
|
|
68
|
+
if (typeof cliente[m] !== "function") faltan.push(m)
|
|
69
|
+
}
|
|
70
|
+
for (const grupo of ["cart", "checkout"]) {
|
|
71
|
+
if (!cliente[grupo] || typeof cliente[grupo] !== "object") {
|
|
72
|
+
faltan.push(grupo)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
for (const m of REQUERIDO[grupo]) {
|
|
76
|
+
if (typeof cliente[grupo][m] !== "function") faltan.push(`${grupo}.${m}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return faltan
|
|
80
|
+
}
|