@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 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 `content` son opcionales: un backend puede no tener cuentas de
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.1",
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",
@@ -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>