@pcreative/commerce-contract 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/package.json +1 -1
- package/src/adapters/api.js +114 -1
- package/src/index.d.ts +154 -0
package/README.md
CHANGED
|
@@ -44,6 +44,31 @@ al tema. Un carrito que ya no existe devuelve `null`, no una excepción.
|
|
|
44
44
|
**`checkout.complete()` puede devolver `{ order }` o `{ cart, error }`** — las
|
|
45
45
|
dos con HTTP 200. Si el cobro falla, el carrito vuelve intacto y con el motivo.
|
|
46
46
|
|
|
47
|
+
**El autor de una pieza y su soporte van juntos.** `getAuthor(handle)` trae la
|
|
48
|
+
ficha pública de quien vende en el mercado, con sus piezas y con lo que calcula
|
|
49
|
+
la plataforma: cuántas consultas resuelve y cuánto tarda en contestar. Lo que
|
|
50
|
+
todavía no tiene dato viene en `null` —nunca en cero— para que el tema pueda
|
|
51
|
+
enseñar «autor nuevo» en vez de un 0 % que espanta.
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const autor = await commerce.getAuthor("estudio-demo")
|
|
55
|
+
autor.support?.resolvedPct // 80, o null si aún no hay tres valoraciones
|
|
56
|
+
autor.sales // 12, o null si todavía no ha vendido
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Para escribir al autor hace falta la clave de licencia**: solo da soporte a
|
|
60
|
+
quien ha comprado. El hilo es privado y el correo del comprador viaja siempre en
|
|
61
|
+
el cuerpo, nunca en la dirección.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const { id, covered } = await commerce.support.open({
|
|
65
|
+
licenseKey, subject: "No instala", message: "Me da un error", email,
|
|
66
|
+
})
|
|
67
|
+
// covered === false: su soporte incluido ya caducó. Se avisa, no se bloquea.
|
|
68
|
+
await commerce.support.get(id, email)
|
|
69
|
+
await commerce.support.rate(id, email, true) // una sola pregunta al cerrar
|
|
70
|
+
```
|
|
71
|
+
|
|
47
72
|
## Escribir otro adaptador
|
|
48
73
|
|
|
49
74
|
Implementa la interfaz `CommerceClient` de [`src/index.d.ts`](src/index.d.ts) y
|
|
@@ -54,8 +79,8 @@ import { assertCommerceClient } from "@pcreative/commerce-contract"
|
|
|
54
79
|
const faltan = assertCommerceClient(miCliente) // [] si cumple
|
|
55
80
|
```
|
|
56
81
|
|
|
57
|
-
`account` y `
|
|
58
|
-
cliente ni blog.
|
|
82
|
+
`account`, `content`, `getAuthor` y `support` son opcionales: un backend puede
|
|
83
|
+
no tener cuentas de cliente, ni blog, ni mercado de autores.
|
|
59
84
|
|
|
60
85
|
## Tests
|
|
61
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pcreative/commerce-contract",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Contrato de datos de commerce: los tipos que consume un tema y los adaptadores que los resuelven. Sin dependencias ni SDK del backend.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
package/src/adapters/api.js
CHANGED
|
@@ -6,6 +6,26 @@ const ORDEN_DEL_CONTRATO = {
|
|
|
6
6
|
price_desc: "precio_desc",
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
const DE_QUIEN = { comprador: "buyer", autor: "author", tienda: "store" }
|
|
10
|
+
|
|
11
|
+
function aHilo(h) {
|
|
12
|
+
return {
|
|
13
|
+
id: h.id,
|
|
14
|
+
subject: h.asunto,
|
|
15
|
+
item: h.pieza,
|
|
16
|
+
status: h.estado,
|
|
17
|
+
covered: h.con_soporte !== false,
|
|
18
|
+
solved: h.resuelto ?? null,
|
|
19
|
+
messages: (h.mensajes ?? []).map((m) => ({
|
|
20
|
+
from: DE_QUIEN[m.de] ?? "store",
|
|
21
|
+
text: m.texto,
|
|
22
|
+
createdAt: m.creado_en,
|
|
23
|
+
})),
|
|
24
|
+
createdAt: h.creado_en,
|
|
25
|
+
updatedAt: h.movido_en,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
export function createCommerce({
|
|
10
30
|
baseUrl,
|
|
11
31
|
publishableKey,
|
|
@@ -108,11 +128,13 @@ export function createCommerce({
|
|
|
108
128
|
tiers: v.tiers ?? [],
|
|
109
129
|
digital: Boolean(v.digital),
|
|
110
130
|
requiresShipping: v.requiresShipping ?? !v.digital,
|
|
131
|
+
code: v.code ?? null,
|
|
111
132
|
bundle: v.bundle ?? null,
|
|
112
133
|
bundleGroups: v.bundleGroups ?? null,
|
|
113
134
|
measure: v.measure ?? null,
|
|
114
135
|
unitPrice: v.unitPrice ?? null,
|
|
115
136
|
})),
|
|
137
|
+
market: p.market ?? null,
|
|
116
138
|
categories: p.categories ?? [],
|
|
117
139
|
quantityRules: p.quantityRules ?? null,
|
|
118
140
|
customization: p.customization ?? [],
|
|
@@ -359,6 +381,11 @@ export function createCommerce({
|
|
|
359
381
|
})
|
|
360
382
|
return mapCart(c)
|
|
361
383
|
},
|
|
384
|
+
async listProvinces(country) {
|
|
385
|
+
if (!country) return []
|
|
386
|
+
const { provincias = [] } = await api(`/tienda/provincias?pais=${encodeURIComponent(country)}`)
|
|
387
|
+
return provincias.map((p) => ({ code: p.codigo, name: p.nombre }))
|
|
388
|
+
},
|
|
362
389
|
async listShippingOptions(cartId) {
|
|
363
390
|
const { moneda = "eur", opciones = [] } = await api(rutaCarrito(cartId, "/envios"))
|
|
364
391
|
return opciones.map((o) => ({
|
|
@@ -566,13 +593,91 @@ export function createCommerce({
|
|
|
566
593
|
throw e
|
|
567
594
|
}
|
|
568
595
|
},
|
|
596
|
+
async getCodes(orderId) {
|
|
597
|
+
try {
|
|
598
|
+
const { codigos } = await api(`/tienda/pedidos/${encodeURIComponent(orderId)}/codigos`)
|
|
599
|
+
return codigos ?? []
|
|
600
|
+
} catch (e) {
|
|
601
|
+
if (e.status === 404) return []
|
|
602
|
+
throw e
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
async revealCode(orderId, codeId) {
|
|
606
|
+
const { codigo } = await api(
|
|
607
|
+
`/tienda/pedidos/${encodeURIComponent(orderId)}/codigos/${encodeURIComponent(codeId)}/revelar`,
|
|
608
|
+
{ method: "POST", body: {} },
|
|
609
|
+
)
|
|
610
|
+
return codigo ?? null
|
|
611
|
+
},
|
|
612
|
+
async getAuthor(handle) {
|
|
613
|
+
try {
|
|
614
|
+
const { autor } = await api(`/tienda/autores/${encodeURIComponent(handle)}`)
|
|
615
|
+
if (!autor) return null
|
|
616
|
+
return {
|
|
617
|
+
handle: autor.handle,
|
|
618
|
+
name: autor.name,
|
|
619
|
+
bio: autor.bio ?? null,
|
|
620
|
+
website: autor.website ?? null,
|
|
621
|
+
country: autor.country ?? null,
|
|
622
|
+
links: autor.links ?? {},
|
|
623
|
+
logo: autor.logo ?? null,
|
|
624
|
+
since: autor.since,
|
|
625
|
+
items: (autor.items ?? []).map((i) => ({
|
|
626
|
+
handle: i.handle,
|
|
627
|
+
title: i.title,
|
|
628
|
+
type: i.type,
|
|
629
|
+
summary: i.summary ?? null,
|
|
630
|
+
cover: i.cover ?? null,
|
|
631
|
+
price: money(i.price?.amount ?? 0, i.price?.currency ?? "eur"),
|
|
632
|
+
})),
|
|
633
|
+
sales: autor.sales ?? null,
|
|
634
|
+
support: autor.soporte
|
|
635
|
+
? {
|
|
636
|
+
resolvedPct: autor.soporte.porcentaje ?? null,
|
|
637
|
+
firstReplyHours: autor.soporte.horasPrimeraRespuesta ?? null,
|
|
638
|
+
ratings: autor.soporte.valorados ?? 0,
|
|
639
|
+
}
|
|
640
|
+
: null,
|
|
641
|
+
}
|
|
642
|
+
} catch (e) {
|
|
643
|
+
if (e.status === 404) return null
|
|
644
|
+
throw e
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
support: {
|
|
648
|
+
async open({ licenseKey, subject, message, email }) {
|
|
649
|
+
const r = await api("/tienda/soporte", {
|
|
650
|
+
method: "POST",
|
|
651
|
+
body: { clave: licenseKey, asunto: subject, texto: message, email },
|
|
652
|
+
})
|
|
653
|
+
return { id: r.id, covered: r.conSoporte !== false }
|
|
654
|
+
},
|
|
655
|
+
async get(id, email) {
|
|
656
|
+
try {
|
|
657
|
+
const { hilo } = await api(`/tienda/soporte/${encodeURIComponent(id)}`, { method: "POST", body: { email } })
|
|
658
|
+
return hilo ? aHilo(hilo) : null
|
|
659
|
+
} catch (e) {
|
|
660
|
+
if (e.status === 404 || e.status === 400) return null
|
|
661
|
+
throw e
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
async reply(id, email, message) {
|
|
665
|
+
await api(`/tienda/soporte/${encodeURIComponent(id)}`, { method: "POST", body: { email, texto: message } })
|
|
666
|
+
},
|
|
667
|
+
async rate(id, email, solved) {
|
|
668
|
+
await api(`/tienda/soporte/${encodeURIComponent(id)}`, {
|
|
669
|
+
method: "POST",
|
|
670
|
+
body: { email, resuelto: !!solved },
|
|
671
|
+
})
|
|
672
|
+
},
|
|
673
|
+
},
|
|
569
674
|
cart,
|
|
570
675
|
checkout,
|
|
571
676
|
account,
|
|
572
677
|
content,
|
|
573
678
|
async getStore() {
|
|
574
679
|
try {
|
|
575
|
-
const { tienda, legal } = await api("/tienda/tienda")
|
|
680
|
+
const { tienda, legal, precios } = await api("/tienda/tienda")
|
|
576
681
|
if (!tienda) return null
|
|
577
682
|
return {
|
|
578
683
|
name: tienda.nombre ?? null,
|
|
@@ -582,6 +687,14 @@ export function createCommerce({
|
|
|
582
687
|
email: tienda.correo ?? null,
|
|
583
688
|
phone: tienda.telefono ?? null,
|
|
584
689
|
country: tienda.pais ?? null,
|
|
690
|
+
priceDisplay: precios
|
|
691
|
+
? {
|
|
692
|
+
show: precios.muestra === 'sin' ? 'without' : precios.muestra === 'ambos' ? 'both' : 'with',
|
|
693
|
+
storedWithTax: !!precios.guardadosConImpuesto,
|
|
694
|
+
defaultRate: Number(precios.tasaPorDefecto ?? 0),
|
|
695
|
+
country: precios.pais ?? null,
|
|
696
|
+
}
|
|
697
|
+
: null,
|
|
585
698
|
legal: legal ?? null,
|
|
586
699
|
}
|
|
587
700
|
} catch {
|
package/src/index.d.ts
CHANGED
|
@@ -53,6 +53,12 @@ export interface Variant {
|
|
|
53
53
|
tiers?: PriceTier[]
|
|
54
54
|
digital?: boolean
|
|
55
55
|
requiresShipping?: boolean
|
|
56
|
+
/**
|
|
57
|
+
* Cuando la variante se vende como codigo: donde se canjea, para que region
|
|
58
|
+
* vale y en que idiomas viene. Se ensena ANTES de pagar; una clave de la
|
|
59
|
+
* region equivocada no sirve y acaba en devolucion.
|
|
60
|
+
*/
|
|
61
|
+
code?: { platform: string | null; region: string | null; languages: string[]; instructions: string | null } | null
|
|
56
62
|
bundle?: BundleComponent[] | null
|
|
57
63
|
bundleGroups?: BundleGroup[] | null
|
|
58
64
|
measure?: { quantity: number; unit: string } | null
|
|
@@ -86,6 +92,47 @@ export interface BundleComponent {
|
|
|
86
92
|
image?: string | null
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Un codigo que se vende y se entrega: clave de juego, tarjeta, licencia.
|
|
97
|
+
* `code` solo llega cuando el comprador pide destaparlo; hasta entonces se
|
|
98
|
+
* ensena `hint` (los ultimos caracteres) y `revealed` dice si ya se destapo,
|
|
99
|
+
* que es lo que cuenta como entregado.
|
|
100
|
+
*/
|
|
101
|
+
export interface ProductCode {
|
|
102
|
+
id: string
|
|
103
|
+
lineId: string
|
|
104
|
+
title: string
|
|
105
|
+
status: "libre" | "reservado" | "entregado" | "revocado" | "retirado"
|
|
106
|
+
hint: string
|
|
107
|
+
code: string | null
|
|
108
|
+
revealed: boolean
|
|
109
|
+
revealedAt: string | null
|
|
110
|
+
deliveredAt: string | null
|
|
111
|
+
reason: string | null
|
|
112
|
+
platform: string | null
|
|
113
|
+
region: string | null
|
|
114
|
+
languages: string[]
|
|
115
|
+
instructions: string | null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Lo que anade un producto que viene del mercado de temas y extensiones:
|
|
120
|
+
* version, cuando se actualizo, demo y changelog. Es lo que mira quien compra
|
|
121
|
+
* software y lo que un producto normal no tiene.
|
|
122
|
+
*/
|
|
123
|
+
export interface MarketInfo {
|
|
124
|
+
type: "tema" | "extension"
|
|
125
|
+
author: { name: string; handle: string }
|
|
126
|
+
version: string | null
|
|
127
|
+
updatedAt: string | null
|
|
128
|
+
stack: string | null
|
|
129
|
+
niche: string[]
|
|
130
|
+
license: string | null
|
|
131
|
+
demoUrl: string | null
|
|
132
|
+
screenshots: string[]
|
|
133
|
+
changelog: { version: string; date: string | null; notes: string | null }[]
|
|
134
|
+
}
|
|
135
|
+
|
|
89
136
|
export interface Download {
|
|
90
137
|
id: string
|
|
91
138
|
lineId: string
|
|
@@ -149,6 +196,8 @@ export interface Product {
|
|
|
149
196
|
/** Precio mínimo entre las variantes: el «desde X €» de la tarjeta. */
|
|
150
197
|
price: Money
|
|
151
198
|
variants: Variant[]
|
|
199
|
+
/** Solo cuando el producto es una pieza del mercado de temas y extensiones. */
|
|
200
|
+
market?: MarketInfo | null
|
|
152
201
|
categories: CategoryRef[]
|
|
153
202
|
tags?: string[]
|
|
154
203
|
rating?: number | null
|
|
@@ -244,6 +293,20 @@ export interface StoreInfo {
|
|
|
244
293
|
email: string | null
|
|
245
294
|
phone: string | null
|
|
246
295
|
country?: string | null
|
|
296
|
+
/**
|
|
297
|
+
* Como quiere la tienda que se vean los precios en el escaparate.
|
|
298
|
+
* `show: 'with'` es lo que exige la ley europea para consumidores;
|
|
299
|
+
* `'without'` y `'both'` son para tiendas que venden a empresas.
|
|
300
|
+
* `storedWithTax` dice si el precio que llega ya lleva el impuesto dentro,
|
|
301
|
+
* y `defaultRate` es la tasa del pais de la tienda, para poder calcular el
|
|
302
|
+
* otro importe antes de que el cliente diga donde vive.
|
|
303
|
+
*/
|
|
304
|
+
priceDisplay?: {
|
|
305
|
+
show: 'with' | 'without' | 'both'
|
|
306
|
+
storedWithTax: boolean
|
|
307
|
+
defaultRate: number
|
|
308
|
+
country: string | null
|
|
309
|
+
}
|
|
247
310
|
legal?: {
|
|
248
311
|
digital?: {
|
|
249
312
|
pais: string
|
|
@@ -431,6 +494,70 @@ export interface Page<T> {
|
|
|
431
494
|
|
|
432
495
|
// ── El cliente ────────────────────────────────────────────────────────────
|
|
433
496
|
|
|
497
|
+
|
|
498
|
+
/** Una pieza del autor tal y como se lista en su ficha. */
|
|
499
|
+
export interface AuthorItem {
|
|
500
|
+
handle: string
|
|
501
|
+
title: string
|
|
502
|
+
type: "tema" | "extension"
|
|
503
|
+
summary: string | null
|
|
504
|
+
price: Money
|
|
505
|
+
cover: string | null
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Lo que la plataforma calcula del soporte de un autor. Es lo que le da valor:
|
|
510
|
+
* no lo escribe él. Cuando todavía no hay datos suficientes viene en null, que
|
|
511
|
+
* se enseña como «autor nuevo» y nunca como un cero.
|
|
512
|
+
*/
|
|
513
|
+
export interface AuthorSupport {
|
|
514
|
+
/** Porcentaje de consultas que sus compradores dieron por resueltas. */
|
|
515
|
+
resolvedPct: number | null
|
|
516
|
+
/** Mediana de horas hasta su primera respuesta. */
|
|
517
|
+
firstReplyHours: number | null
|
|
518
|
+
/** Cuántas valoraciones sostienen el porcentaje. */
|
|
519
|
+
ratings: number
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** La ficha pública de quien vende una pieza en el mercado. */
|
|
523
|
+
export interface Author {
|
|
524
|
+
handle: string
|
|
525
|
+
name: string
|
|
526
|
+
bio: string | null
|
|
527
|
+
website: string | null
|
|
528
|
+
/** Código de dos letras, en minúsculas. */
|
|
529
|
+
country: string | null
|
|
530
|
+
/** Redes y sitios del autor, siempre https. */
|
|
531
|
+
links: Record<string, string>
|
|
532
|
+
logo: string | null
|
|
533
|
+
/** Desde cuándo publica. */
|
|
534
|
+
since: string
|
|
535
|
+
items: AuthorItem[]
|
|
536
|
+
/** Solo cuando hay ventas: un cero espanta y no se enseña. */
|
|
537
|
+
sales: number | null
|
|
538
|
+
support: AuthorSupport | null
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export interface SupportMessage {
|
|
542
|
+
from: "buyer" | "author" | "store"
|
|
543
|
+
text: string
|
|
544
|
+
createdAt: string
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export interface SupportThread {
|
|
548
|
+
id: string
|
|
549
|
+
subject: string
|
|
550
|
+
item: string
|
|
551
|
+
status: "abierto" | "contestado" | "esperando" | "cerrado"
|
|
552
|
+
/** Si su soporte incluido sigue vigente. Caducado no cierra la puerta: avisa. */
|
|
553
|
+
covered: boolean
|
|
554
|
+
/** Lo que contestó al cerrar. null mientras no se le ha preguntado. */
|
|
555
|
+
solved: boolean | null
|
|
556
|
+
messages: SupportMessage[]
|
|
557
|
+
createdAt: string
|
|
558
|
+
updatedAt: string
|
|
559
|
+
}
|
|
560
|
+
|
|
434
561
|
export interface CommerceClient {
|
|
435
562
|
/** Identificador del adaptador: "pcreative", "static"… */
|
|
436
563
|
readonly adapter: string
|
|
@@ -447,10 +574,31 @@ export interface CommerceClient {
|
|
|
447
574
|
subirFichero?(archivo: File): Promise<{ id: string; nombre: string; tipo: string; bytes: number; firma: string }>
|
|
448
575
|
/** Opcional: las descargas de un pedido con contenido digital. */
|
|
449
576
|
getDownloads?(orderId: string): Promise<Download[]>
|
|
577
|
+
/** Los codigos de un pedido, tapados. */
|
|
578
|
+
getCodes?(orderId: string): Promise<ProductCode[]>
|
|
579
|
+
/** Destapa uno y devuelve el codigo entero. Queda apuntado quien y cuando. */
|
|
580
|
+
revealCode?(orderId: string, codeId: string): Promise<string>
|
|
450
581
|
/** Opcional: estado del derecho de desistimiento de un pedido. */
|
|
451
582
|
getWithdrawal?(orderId: string): Promise<WithdrawalState | null>
|
|
452
583
|
/** Opcional: solicita el desistimiento de un pedido (botón obligatorio en la UE). */
|
|
453
584
|
requestWithdrawal?(orderId: string, datos?: { lineas?: string[]; motivo?: string }): Promise<{ solicitud: string; aceptadas: number }>
|
|
585
|
+
/** Opcional: la ficha pública de un autor del mercado. */
|
|
586
|
+
getAuthor?(handle: string): Promise<Author | null>
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Opcional: hablar con el autor de una pieza. Es privado y hace falta la
|
|
590
|
+
* clave de licencia, así que solo escribe quien ha comprado.
|
|
591
|
+
*
|
|
592
|
+
* El correo va en el cuerpo y nunca en la dirección: identifica a una
|
|
593
|
+
* persona y una dirección acaba en registros y en el historial del navegador.
|
|
594
|
+
*/
|
|
595
|
+
support?: {
|
|
596
|
+
open(datos: { licenseKey: string; subject: string; message: string; email?: string }): Promise<{ id: string; covered: boolean }>
|
|
597
|
+
get(id: string, email: string): Promise<SupportThread | null>
|
|
598
|
+
reply(id: string, email: string, message: string): Promise<void>
|
|
599
|
+
/** Una sola pregunta al cerrar: si le sirvió. Alimenta el dato público del autor. */
|
|
600
|
+
rate(id: string, email: string, solved: boolean): Promise<void>
|
|
601
|
+
}
|
|
454
602
|
|
|
455
603
|
cart: {
|
|
456
604
|
get(id: string): Promise<Cart | null>
|
|
@@ -472,6 +620,12 @@ export interface CommerceClient {
|
|
|
472
620
|
|
|
473
621
|
checkout: {
|
|
474
622
|
listCountries?(): Promise<string[]>
|
|
623
|
+
/**
|
|
624
|
+
* Provincias, estados o departamentos de un pais, en el nivel que pide su
|
|
625
|
+
* caja (Espana pide provincia, Alemania Land, Malta nada). Lista vacia =
|
|
626
|
+
* ese pais no usa provincia y el tema debe dejar el campo libre.
|
|
627
|
+
*/
|
|
628
|
+
listProvinces?(country: string): Promise<Array<{ code: string; name: string }>>
|
|
475
629
|
setVatNumber?(cartId: string, vatNumber: string | null): Promise<Cart>
|
|
476
630
|
setEmail(cartId: string, email: string): Promise<Cart>
|
|
477
631
|
setAddresses(cartId: string, shipping: Address, billing?: Address): Promise<Cart>
|