@pcreative/commerce-contract 1.3.1 → 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 +100 -0
- package/src/index.d.ts +134 -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 ?? [],
|
|
@@ -571,6 +593,84 @@ export function createCommerce({
|
|
|
571
593
|
throw e
|
|
572
594
|
}
|
|
573
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
|
+
},
|
|
574
674
|
cart,
|
|
575
675
|
checkout,
|
|
576
676
|
account,
|
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
|
|
@@ -445,6 +494,70 @@ export interface Page<T> {
|
|
|
445
494
|
|
|
446
495
|
// ── El cliente ────────────────────────────────────────────────────────────
|
|
447
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
|
+
|
|
448
561
|
export interface CommerceClient {
|
|
449
562
|
/** Identificador del adaptador: "pcreative", "static"… */
|
|
450
563
|
readonly adapter: string
|
|
@@ -461,10 +574,31 @@ export interface CommerceClient {
|
|
|
461
574
|
subirFichero?(archivo: File): Promise<{ id: string; nombre: string; tipo: string; bytes: number; firma: string }>
|
|
462
575
|
/** Opcional: las descargas de un pedido con contenido digital. */
|
|
463
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>
|
|
464
581
|
/** Opcional: estado del derecho de desistimiento de un pedido. */
|
|
465
582
|
getWithdrawal?(orderId: string): Promise<WithdrawalState | null>
|
|
466
583
|
/** Opcional: solicita el desistimiento de un pedido (botón obligatorio en la UE). */
|
|
467
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
|
+
}
|
|
468
602
|
|
|
469
603
|
cart: {
|
|
470
604
|
get(id: string): Promise<Cart | null>
|