@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 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.0",
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 ?? [],
@@ -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>