@bacano/sdk 1.2.1 → 1.2.3

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/FLOWS.md ADDED
@@ -0,0 +1,114 @@
1
+ # Flows: participant SDK
2
+
3
+ This delivery provides a local integration surface. Production identity and access-management adapters are intentionally not enabled yet. The local gateway returns 404 outside development even if its demo flag is enabled. Do not deploy demo credentials or replace Clerk with this adapter.
4
+
5
+ ```ts
6
+ import { createFlowsClient } from "@bacano/sdk";
7
+
8
+ let token: string | null = null;
9
+ const flows = createFlowsClient({
10
+ apiUrl: "http://localhost:3001",
11
+ getAccessToken: () => token,
12
+ });
13
+ const session = await flows.localDemoLogin("ana@example.test", "Academy123!");
14
+ token = session.token;
15
+ const cases = await flows.listCases();
16
+ const journey = await flows.getJourney(cases[0].id);
17
+ // Render only available steps and the capabilities returned by the server.
18
+ await flows.logout();
19
+ token = null;
20
+ ```
21
+
22
+ Existing Bacano clients expose the same methods on `client.flows`. No direct Hasura access or administrative credential belongs in the website. Bearer identity is resolved by the gateway; request bodies cannot choose a participant.
23
+
24
+ ## Surface
25
+
26
+ - `listCases()`, `getJourney(caseId)`: explicitly granted cases and filtered state.
27
+ - `getContent(caseId, stepId)`: ordered published Content resources compatible with Flows. Private fields are removed by the backend.
28
+ - `getFile(caseId, stepId, fileId)`: authenticated Blob, only for files referenced in accessible content and belonging to the company. Revoke object URLs on unmount/logout.
29
+ - `getForm(caseId, stepId)`: current published form, version and definition hash.
30
+ - `getResponse(caseId, stepId)`: own response or explicitly shared evaluation; historical labels/answers, attempt and submission time. Never internal notes.
31
+ - `act(caseId, command)`: `complete_content`, `complete_task`, `submit_form`. No review/approval/move operation for the participant.
32
+
33
+ Every command supplies `stepId`, `revision` (case), `flowRevision`, and `idempotencyKey`. Keep the **same complete envelope and key** on network retries. New input requires a new key. On revision conflict refresh the journey and ask the person to review before resending. The server rechecks access, assignment, sequence and state; rendered buttons are not authorization.
34
+
35
+ ```tsx
36
+ import { BacanoFlowForm } from "@bacano/sdk/react/forms";
37
+
38
+ <BacanoFlowForm
39
+ key={`${caseId}:${stepId}:${publishedForm.correction?.attempt ?? 0}`}
40
+ form={publishedForm}
41
+ onRefresh={reloadPublishedForm}
42
+ onSubmit={(values, form) => submitStep({
43
+ operation: "submit_form",
44
+ submission: { values, formVersion: form.version, definitionHash: form.definitionHash },
45
+ })}
46
+ />
47
+ ```
48
+
49
+ The component reuses existing Forms controls but never posts to the website-entry endpoint. A publication conflict leaves entered answers in place; refreshing the definition does not clear them. It does not persist sensitive drafts in localStorage. Clearing the authenticated view on logout removes its state. Applications must provide error/loading states and retain an idempotency envelope for retries.
50
+
51
+ ### Corrections
52
+
53
+ After an operator requests changes, `getForm` may include `initialValues` and
54
+ `correction: { attempt, formChanged }`. These are the authenticated participant's
55
+ own previous answers to this case/step/form, filtered against the currently
56
+ published questions and selected conditional branches. They never contain internal
57
+ evaluations or another participant's answers. Numbers/scales use editable strings;
58
+ checkboxes preserve booleans. Removed options and fields are omitted.
59
+
60
+ Initialize a custom renderer from `initialValues ?? {}` once per case/step/attempt.
61
+ Do not overwrite unsent edits during refresh or use case revision as a component
62
+ key. Show a review warning when `formChanged` is true. Submitting creates a new
63
+ immutable response; the earlier attempt remains in the operator history.
64
+ Requesting changes requires participant instructions, not the configured evaluation
65
+ form. Approving still requires that evaluation when configured.
66
+
67
+ ## Stage presentation
68
+
69
+ Each journey stage exposes `presentation`: `step_by_step` (default, including older definitions) or `single_view`. Treat an absent value from an older gateway as `step_by_step`. This preference is independent of sequential/free execution order and manual/automatic advancement.
70
+
71
+ For `single_view`, render the stage's steps together in their existing order, with stable component keys based on case and step IDs. Each activity saves independently; do not introduce a combined submit or automatically complete content when a form is sent.
72
+
73
+ - Unavailable steps may show a locked heading, but must not load their content, form or files.
74
+ - Internal steps may show their public status, never private instructions, evaluations or review controls. Respect `canReadResponse` for explicitly shared responses.
75
+ - Operator-assigned tasks are omitted entirely from the participant journey,
76
+ including their titles. They remain operable in Bacano and still govern
77
+ sequencing/advancement. A stage with only internal tasks may therefore have
78
+ no visible steps: show a waiting state, never advance it in the browser.
79
+ Explicitly participant-assigned tasks remain available to that participant.
80
+ - Refresh the journey after every action. Use the updated revisions for new commands. Concurrent actions may conflict; preserve unsent form values and let the participant retry after refreshing.
81
+ - Keep sibling forms mounted when a step is completed so their local inputs survive. Do not use the case revision as a React key.
82
+ - In single-view mode, keep the current page after saving; the participant can navigate to the next stage using the journey navigation.
83
+
84
+ This is configuration delivered by the SDK, not an automatic layout component. Consuming applications implement the layout; the local Academy example implements both modes.
85
+
86
+ ## Local identity and access
87
+
88
+ - Local demo emails: `ana@example.test`, `luis@example.test`; password `Academy123!`.
89
+ - Enable only the local backend override (`docker-compose.flows-demo.yml`) and an explicit website UUID. Origins must be localhost/loopback.
90
+ - The seed explicitly creates two client/contact fixtures, two separate real cases and grant rows. Runtime access is **not** inferred from email or contact.
91
+ - Opaque local sessions expire after one hour; API restart or logout invalidates them. Session loss never deletes case data.
92
+ - Revoking a `flow_case_access` row blocks subsequent reads and commands without deleting audit history.
93
+ - A manual-advance stage remains there after its required form is submitted. An operator must move it; automatic stages use the existing domain rules. Empty stages do not advance spontaneously.
94
+
95
+ ## Pending before production
96
+
97
+ Verify Clerk through the existing website identity adapter; add operator grant/revoke actions guarded by `flows.share`; validate isolation across real sites/tenants and production accounts; replace local fixtures and test deployment. Do not enable production by weakening the development/origin checks.
98
+
99
+ ## Contenido con devolución relacionada
100
+
101
+ Un paso `content` puede exponer `canReadResponse: true`. En ese caso consulta
102
+ `getContent(caseId, step.id)` y `getResponse(caseId, step.id)` para presentar el
103
+ material de apoyo junto a la devolución original del mismo caso. No uses IDs de
104
+ respuestas ni de otros pasos para saltarte la disponibilidad del recorrido.
105
+
106
+ Si `canComplete` es verdadero y el contenido no es de consulta, ofrece continuar
107
+ mediante `act(..., { operation: "complete_content", ... })` con las revisiones e
108
+ idempotency key vigentes. La aprobación interna entrega el resultado; esta acción
109
+ registra por separado la confirmación del participante. No marques el paso como
110
+ completado por abrirlo. Si la respuesta no pudo cargarse, no ofrezcas confirmarla.
111
+
112
+ Tras cada acción vuelve a consultar el recorrido y presenta su etapa actual.
113
+ Durante una espera interna puedes consultar periódicamente el estado; conserva
114
+ los formularios con valores sin enviar que sigan en la misma etapa agrupada.
package/README.md CHANGED
@@ -7,6 +7,10 @@ Buyer credentials are owned by each storefront's Clerk application. Read
7
7
  sign-in, customer profiles, authenticated checkout, or order history. The guide
8
8
  ships inside the npm package.
9
9
 
10
+ For participant journeys, forms and activities, see [FLOWS.md](./FLOWS.md).
11
+ Flows currently includes an explicitly gated local identity adapter; production
12
+ participant authentication and grant management are not enabled by this sample.
13
+
10
14
  ## Installation
11
15
 
12
16
  ```bash
@@ -107,6 +111,12 @@ when instrumenting analytics, advertising signals, contact actions, ecommerce
107
111
  events, attribution, or preview behavior. It is the canonical implementation
108
112
  contract for storefront agents and ships inside the npm package.
109
113
 
114
+ Every storefront that accepts payments must follow
115
+ [`STOREFRONT_PAYMENTS.md`](./STOREFRONT_PAYMENTS.md). It defines the SDK flow,
116
+ provider boundaries, Mercado Pago Orders setup and payload invariants, safe
117
+ retry behavior, diagnostics and the minimum sandbox evidence required before
118
+ production.
119
+
110
120
  Commercial forms and newsletter subscriptions must follow
111
121
  [`STOREFRONT_LEADS.md`](./STOREFRONT_LEADS.md). Storefronts own the form UI;
112
122
  Bacano owns consent evidence, identity resolution, attribution and idempotent
@@ -639,9 +649,16 @@ ahora" and "Pagar al recoger", while `SHIPPING` may return "Pagar ahora" and
639
649
  "Pagar contraentrega".
640
650
 
641
651
  Only payment methods marked as **Disponible para web** in Bacano and enabled in
642
- the website payment settings are returned. Gateway methods such as Wompi expose
643
- `gatewayCode`, `requiresPaymentSession` and public configuration only; secrets
644
- never leave Bacano.
652
+ the website payment settings are returned. Gateway methods such as Wompi,
653
+ Mercado Pago, Openpay and Addi expose `gatewayCode`,
654
+ `requiresPaymentSession` and public configuration only; secrets never leave
655
+ Bacano.
656
+
657
+ Openpay uses its hosted Webcheckout. The storefront does not render Openpay.js,
658
+ does not request card/PSE data and does not send a provider transaction ID when
659
+ reconciling the return. Open the Bacano `redirectUrl` immediately and let the
660
+ backend verify the charge. See [STOREFRONT_PAYMENTS.md](./STOREFRONT_PAYMENTS.md)
661
+ for the provider-specific contract and retry rules.
645
662
 
646
663
  Each payment option also exposes `initialPaymentPolicy` (`none`, `optional` or
647
664
  `required`), `requiresInitialPayment`, `minInitialAmount` and
@@ -657,6 +674,60 @@ initialPayment: {
657
674
  The API enforces the policy, minimum and order total. Do not infer deposits from
658
675
  labels or delivery mode.
659
676
 
677
+ #### Addi: conditional buyer identification
678
+
679
+ The Addi backend adapter is implemented on the integration branch and internally
680
+ tested. It still requires migration, merchant configuration and sandbox validation
681
+ before use. Render only methods returned by Bacano, never invent an Addi option:
682
+
683
+ - When the selected method has `gatewayCode === "ADDI"`, show the identification
684
+ fields immediately below that payment method, before submitting the order.
685
+ - Request **Cédula de ciudadanía** only if a compatible document is missing.
686
+ Reuse an existing CC without asking the buyer to enter it twice; allow correction.
687
+ - Addi only accepts CC in this integration. Show the document type as the fixed
688
+ label **Cédula de ciudadanía (CC)**, not a selector offering unsupported types.
689
+ The editable field is **Número de cédula**; no document photo is requested here.
690
+ - Submit identification through the existing `contact.documentType` and
691
+ `contact.documentNumber` fields. Use Bacano's document-type identifiers from
692
+ `customerIdentification`; the backend adapter must map the CC identifier to
693
+ Addi's `idType: "CC"`. Do not assume the two systems use identical codes.
694
+ - Switching away from Addi removes its extra identification requirement and
695
+ hides fields added only for Addi. Keep any identification fields required by
696
+ the website's own `customerIdentification` policy.
697
+ - Validate missing/incompatible identification before proceeding, with an
698
+ inline message explaining that Addi requires a CC. Backend validation must
699
+ enforce the same rule; hiding a field is not a security boundary.
700
+
701
+ Suggested helper text: “Para continuar con Addi, necesitamos tu número de
702
+ cédula. Completarás el proceso de financiación en Addi.”
703
+
704
+ This condition concerns identification only; the integration must also validate
705
+ the other buyer data required by Addi. Credit evaluation and financing remain
706
+ in Addi's hosted checkout, not in a custom storefront credit form.
707
+ For Addi, also provide `contact.firstName`, `lastName`, `email`, a Colombian
708
+ `cellphone`, and residence in `contact.address.line1` and
709
+ `contact.address.municipalityName`. Ask explicitly before reusing a delivery
710
+ address as residence. These fields already exist in the SDK contact contract.
711
+ This first adapter supports complete product-order payments in COP, not deposits
712
+ or service orders. Errors must remain visible; never mark a payment approved from
713
+ redirect parameters. Use the backend's order status.
714
+ See the [official Addi API](https://api-docs.addi-staging.com/integration/).
715
+
716
+ #### Payment-method surcharge (planned; not yet available)
717
+
718
+ The planned company payment-method setting is **Recargo al comprador (%)**,
719
+ defaulting to **0 %**, shared by web and point of sale. This is a merchant
720
+ surcharge, not the provider's financing interest or settlement commission.
721
+ The agreed basis is products after discounts, excluding shipping.
722
+ Provider/merchant contractual eligibility and the calculation/tax rules must be
723
+ confirmed before enabling it.
724
+
725
+ Once supported, storefronts must show the backend-calculated surcharge and final
726
+ total when a method is selected, before the buyer confirms. Switching methods
727
+ must remove/recalculate it; 0 % adds no charge. Do not calculate a surcharge
728
+ locally or add it directly to the gateway amount. There are no surcharge SDK
729
+ fields or operations available yet; the response contract remains to be defined.
730
+
660
731
  ```typescript
661
732
  const payNow = paymentOptions.find((option) => option.mode === "pay_now");
662
733
 
@@ -945,6 +1016,10 @@ contains only browser-safe media metadata and a public Storage URL.
945
1016
 
946
1017
  GA4, Meta, and Microsoft Clarity are configured per website in Bacano and are
947
1018
  disabled by default.
1019
+ An optional Google Tag Manager web container is also supported via
1020
+ `marketing.destinations.gtm.containerId`. It adds `bacano_*` browser events to
1021
+ the data layer without replacing native destinations or server conversions.
1022
+ See [GTM setup, limitations and duplication safeguards](./STOREFRONT_MARKETING.md#google-tag-manager-contenedor-web-opcional).
948
1023
  The existing public-config request returns only active public destination IDs;
949
1024
  provider secrets never reach the SDK. The runtime remains a no-op outside the
950
1025
  website's canonical production domain.
@@ -1533,22 +1608,22 @@ function ProfilePage() {
1533
1608
 
1534
1609
  ### Hooks Summary
1535
1610
 
1536
- | Hook | Returns | Auth Required |
1537
- | ----------------------------- | --------------------------------------------------------------------------------------- | ------------- |
1538
- | `useBacano()` | `BacanoClient` | No |
1539
- | `useBacanoState()` | `{ client, loading, error }` | No |
1540
- | `useProducts(opts?)` | `{ data, loading, error, refetch }` | No |
1541
- | `useProduct(id)` | `{ data, loading, error, refetch }` | No |
1542
- | `useCategories()` | `{ data, loading, error, refetch }` | No |
1543
- | `useBrands()` | `{ data, loading, error, refetch }` | No |
1544
- | `useProductFilters(opts?)` | `{ data, loading, error, refetch }` | No |
1545
- | `useSearchSuggestions(opts?)` | `{ data, loading, error, refetch }` | No |
1546
- | `useAvailability(ids)` | `{ data, loading, error, refetch }` | No |
1611
+ | Hook | Returns | Auth Required |
1612
+ | ----------------------------- | ---------------------------------------------------------------------------------------------- | ------------- |
1613
+ | `useBacano()` | `BacanoClient` | No |
1614
+ | `useBacanoState()` | `{ client, loading, error }` | No |
1615
+ | `useProducts(opts?)` | `{ data, loading, error, refetch }` | No |
1616
+ | `useProduct(id)` | `{ data, loading, error, refetch }` | No |
1617
+ | `useCategories()` | `{ data, loading, error, refetch }` | No |
1618
+ | `useBrands()` | `{ data, loading, error, refetch }` | No |
1619
+ | `useProductFilters(opts?)` | `{ data, loading, error, refetch }` | No |
1620
+ | `useSearchSuggestions(opts?)` | `{ data, loading, error, refetch }` | No |
1621
+ | `useAvailability(ids)` | `{ data, loading, error, refetch }` | No |
1547
1622
  | `useCart()` | `{ cart, loading, error, startCheckout, getOrCreate, addItem, updateItem, removeItem, clear }` | No |
1548
- | `useCheckout()` | `{ checkout, loading, error }` | No |
1549
- | `useOrders(opts?)` | `{ data, loading, error, refetch }` | Yes |
1550
- | `useOrder(id)` | `{ data, loading, error, refetch }` | Yes |
1551
- | `useProfile()` | `{ profile, loading, error, updateProfile }` | Yes |
1623
+ | `useCheckout()` | `{ checkout, loading, error }` | No |
1624
+ | `useOrders(opts?)` | `{ data, loading, error, refetch }` | Yes |
1625
+ | `useOrder(id)` | `{ data, loading, error, refetch }` | Yes |
1626
+ | `useProfile()` | `{ profile, loading, error, updateProfile }` | Yes |
1552
1627
 
1553
1628
  ---
1554
1629
 
@@ -1,5 +1,19 @@
1
1
  # Formularios de websites
2
2
 
3
+ ## Escalas discretas (desarrollo local)
4
+
5
+ El tipo `scale` incorpora `scale: { min, max, display, minLabel?, maxLabel? }`.
6
+ `display` admite `numbers` o `stars`. Los límites son enteros entre 0 y 10,
7
+ con mínimo menor al máximo; las estrellas comienzan en 1. No usa `optionSource`
8
+ ni `validation`: sus límites viven en `scale`. Ejemplo: `scale: { min: 1, max: 5, display: "stars" }`.
9
+
10
+ El renderer muestra una selección única, sin valor inicial. Envía el entero elegido
11
+ como string (`"4"`), igual que los campos numéricos existentes. El backend valida
12
+ el rango, guarda el número y conserva `4 / 5` en el snapshot histórico. Funciona
13
+ tanto en formularios web como en pasos de Flows. No es un slider continuo ni una
14
+ calificación calculada automáticamente. Renderers propios deben respetar el
15
+ rango, `required`, las etiquetas y la interacción por teclado.
16
+
3
17
  Desde SDK 1.2.0, `BacanoForm` instrumenta interacciones mediante el provider
4
18
  global de marketing. Para modales montados ocultos, pasar `visible={open}`.
5
19
  Renderers personalizados usan `useBacanoFormTracking`. Leer la sección de
@@ -250,6 +264,42 @@ consultarlo.
250
264
  no recibe ni debe inferir si Bacano creó un lead, encontró un cliente o marcó
251
265
  una identidad para revisión.
252
266
 
267
+ ## Preguntas condicionales
268
+
269
+ Un campo `radio` o `select` con opciones inline puede incluir ramas recursivas:
270
+
271
+ ```ts
272
+ branches: [
273
+ { value: "simple", fields: [/* campos normales */] },
274
+ { value: "complete", fields: [/* campos normales o condicionales */] },
275
+ ]
276
+ ```
277
+
278
+ `value` corresponde al valor estable de una opción. La etiqueta puede cambiar sin
279
+ mover sus campos. Las definiciones sin `branches` siguen funcionando igual.
280
+ `BacanoForm` y el renderer de formularios de Flows muestran solo el camino elegido,
281
+ aplican la obligatoriedad de sus hijos y descartan valores ocultos al cambiar de
282
+ opción. El backend vuelve a validarlo; no acepta respuestas de caminos ocultos.
283
+
284
+ Para un renderer propio, importar desde la entrada principal del SDK:
285
+
286
+ ```ts
287
+ import { activeFormValues, formRootFields, visibleFormFields } from "@bacano/sdk";
288
+
289
+ const fields = visibleFormFields(formRootFields(definition), values);
290
+ const submissionValues = activeFormValues(definition, values);
291
+ ```
292
+
293
+ `visibleFormFields` devuelve la ruta activa aplanada, incluyendo las preguntas
294
+ padre. Para preservar secciones, pasos y presentación, recorrer cada contenedor
295
+ y sus ramas seleccionadas. Al modificar una respuesta, usar `activeFormValues`
296
+ también para actualizar el estado: evita restaurar respuestas ocultas al volver
297
+ a una opción anterior. Filtrar nuevamente antes de enviar.
298
+
299
+ Los sitios que tengan instalado un SDK anterior deben actualizarlo. Una definición
300
+ con ramas necesita un renderer compatible; no basta con actualizar el backend.
301
+ La definición admite cinco niveles condicionales y cien campos en total.
302
+
253
303
  ## Checklist mínimo del storefront
254
304
 
255
305
  - Usar un solo `BacanoProvider` y el cliente global ya inicializado.
@@ -6,9 +6,65 @@ identificadores, adaptadores ni llamadas directas a proveedores.
6
6
 
7
7
  ## Regla principal
8
8
 
9
+ ### Google Tag Manager (contenedor web opcional)
10
+
11
+ En Bacano, la tarjeta Google Tag Manager permite guardar un ID `GTM-…`, validar
12
+ que el script publicado sea accesible y activar o pausar la integración por sitio.
13
+ Validar no comprueba propiedad del contenedor, etiquetas, consentimiento ni
14
+ recepción de eventos: comprobar esto en Tag Assistant con el administrador del
15
+ contenedor. No se solicitan secretos, HTML ni URLs de scripts personalizados.
16
+
17
+ Requiere desplegar API/migración y actualizar `@bacano/sdk` en el storefront.
18
+ El contrato público añade `marketing.destinations.gtm.containerId`; SDKs anteriores
19
+ lo ignoran. Un sitio con configuración estática debe reconstruirse para incorporar
20
+ cambios. No se modifica ni se publica automáticamente ningún contenedor de Google.
21
+
22
+ El mismo `BacanoMarketingProvider` carga `gtm.js` una sola vez, conserva la cola
23
+ `window.dataLayer` existente y respeta las barreras de dominio, preview y
24
+ `measurementAllowed`, además de la carga diferida. No instalar otro snippet.
25
+ Esta versión requiere JavaScript; no incorpora un iframe `noscript` ni GTM server-side.
26
+ Pausar evita nuevas cargas con la configuración actualizada; no puede deshacer
27
+ etiquetas de terceros ya ejecutadas en pestañas abiertas. Recargar para comprobar.
28
+
29
+ Los eventos oficiales de navegador se publican con prefijo `bacano_`:
30
+
31
+ ```js
32
+ { event: "bacano_add_to_cart", bacano: {
33
+ schema_version: 1, event_name: "add_to_cart", event_id: "…",
34
+ currency: "COP", value: 50000,
35
+ items: [{ item_id: "product-variant-id", item_name: "Producto", quantity: 1 }]
36
+ } }
37
+ ```
38
+
39
+ Configurar activadores de evento personalizado `bacano_*` y variables de capa de
40
+ datos como `bacano.event_name`, `bacano.value` y `bacano.items`. El objeto `bacano`
41
+ se limpia antes de cada evento para no arrastrar artículos ni parámetros anteriores.
42
+ Incluye URL saneada, parámetros de comercio e identificadores técnicos de
43
+ interacciones; no copia datos de clientes, valores de formularios, atribución,
44
+ títulos de página ni texto libre de búsqueda. Los eventos `search` no llevan el término.
45
+ No emitir desde GTM `purchase`, `refund` u `order_placed` basándose en la página de
46
+ gracias: siguen siendo autoritativos del servidor y no se publican en este puente.
47
+
48
+ GA4, Meta y Clarity nativos permanecen sin cambios. **No repetir esos destinos o
49
+ conversiones dentro del contenedor**. Esta fase no es un modo alternativo completo
50
+ de medición ni realiza deduplicación entre proveedores y etiquetas configuradas
51
+ externamente. El administrador de GTM puede ejecutar scripts con acceso a la página:
52
+ otorgar acceso solo a personas de confianza y revisar cada publicación.
53
+ El aviso de cookies informativo de Bacano no es un CMP y no autoriza las etiquetas;
54
+ el responsable del contenedor debe configurar el consentimiento aplicable.
55
+
56
+ CSP: permitir `https://www.googletagmanager.com` en `script-src`. Los destinos
57
+ adicionales y Tag Assistant requieren revisar sus propios permisos de CSP; no
58
+ habilitar comodines ni `unsafe-eval` globalmente para acomodar un contenedor.
59
+
60
+ Referencias: [configuración de GTM](https://support.google.com/tagmanager/answer/14842164)
61
+ y [contrato dataLayer](https://developers.google.com/tag-platform/tag-manager/datalayer).
62
+
63
+ ### Eventos comunes
64
+
9
65
  El storefront expresa hechos mediante `useMarketing().track()`. El SDK decide
10
- si la medición está activa, sanea la URL y adapta el evento a GA4, Meta y
11
- Microsoft Clarity. Nunca
66
+ si la medición está activa, sanea la URL y adapta el evento a GA4, Meta,
67
+ Microsoft Clarity y, si se configuró, GTM. Nunca
12
68
  se debe llamar directamente a `gtag`, `dataLayer`, `fbq`, Measurement Protocol
13
69
  o Conversions API.
14
70
  Clarity también se carga desde este runtime; no se instala su snippet de forma
@@ -0,0 +1,231 @@
1
+ # Pagos en storefronts Bacano
2
+
3
+ Contrato canónico para agentes que implementan o auditan pagos con
4
+ `@bacano/sdk`. El storefront presenta las opciones y abre la URL devuelta por
5
+ Bacano; las credenciales, payloads, webhooks, conciliación, inventario y
6
+ contabilidad pertenecen al backend.
7
+
8
+ ## Flujo obligatorio del SDK
9
+
10
+ 1. Consultar `checkout.getPaymentOptions({ fulfillmentFamily })` y mostrar
11
+ únicamente los métodos retornados por Bacano.
12
+ 2. Enviar `paymentMode` y `paymentMethodId` a `checkout.submit()`.
13
+ 3. Si el resultado exige pasarela, llamar una sola vez a
14
+ `checkout.createPaymentSession()` con el `orderId`, `publicOrderToken`,
15
+ método y una `returnUrl` pública.
16
+ 4. Cuando `integrationMode === "redirect"`, navegar inmediatamente a
17
+ `redirectUrl` con `window.location.assign()`. No insertar una pantalla
18
+ intermedia de confirmación.
19
+ 5. En el retorno, conciliar la sesión existente y después consultar el pedido.
20
+ Nunca crear otro pedido para reintentar el mismo pago.
21
+ 6. Aprobar la compra solo por el estado verificado por Bacano. Los parámetros
22
+ del navegador y la pantalla del proveedor no son evidencia financiera.
23
+
24
+ El storefront no construye URLs de Wompi, Mercado Pago, Openpay o Addi, no envía
25
+ credenciales del comercio y no reproduce los payloads privados del proveedor.
26
+
27
+ ### Conciliación segura al regresar del proveedor
28
+
29
+ La llamada canónica es:
30
+
31
+ ```ts
32
+ await client.checkout.reconcilePaymentSession({
33
+ orderId,
34
+ publicOrderToken,
35
+ paymentSessionId,
36
+ // Solo si el proveedor devuelve un ID que Bacano espera y verifica.
37
+ gatewayTransactionId,
38
+ });
39
+ ```
40
+
41
+ - `paymentSessionId` es la referencia estable creada por Bacano y debe viajar
42
+ en la `returnUrl`.
43
+ - Para **Mercado Pago Orders**, **Openpay** y **Addi**, se omite
44
+ `gatewayTransactionId`: Bacano recupera el identificador remoto guardado y
45
+ consulta al proveedor con credenciales del servidor.
46
+ - Para **Wompi**, se envía el ID público de transacción retornado por Wompi;
47
+ Bacano comprueba que coincida antes de consultar la pasarela.
48
+ - No tratar `payment_id`, `collection_status`, `status`, ni ningún otro
49
+ parámetro del navegador como evidencia de aprobación. En Mercado Pago,
50
+ `payment_id` identifica un pago dentro de la Order y no sustituye el ID de
51
+ Order remoto almacenado por Bacano.
52
+ - Incluso si la conciliación responde con error temporal, consultar el pedido
53
+ y esperar el estado confirmado mediante `orders.waitForPayment()`; un
54
+ timeout significa **pendiente**, no rechazado.
55
+
56
+ ## Mercado Pago · contrato vigente
57
+
58
+ La integración de Bacano usa **Checkout Pro con API de Orders** (`POST
59
+ /v1/orders`), no la API antigua de Preferences. Esta diferencia es crítica:
60
+ una aplicación o payload de Preferences puede autenticar correctamente y aun
61
+ así recibir `403` o `400` al crear la order.
62
+
63
+ ### Configuración del comercio
64
+
65
+ - Crear o elegir una aplicación en **Mercado Pago Developers** preparada para
66
+ Checkout Pro y **API de Orders**.
67
+ - En Bacano, **Access Token de la aplicación** significa la credencial privada
68
+ con la que el backend llama la API. No es la Public Key y tampoco es la firma
69
+ del webhook.
70
+ - **Firma secreta del Webhook** es un secreto distinto, generado al guardar la
71
+ configuración de notificaciones en Mercado Pago.
72
+ - Pruebas y producción tienen credenciales diferentes. El ambiente elegido en
73
+ Bacano debe coincidir con el vendedor/credencial verificado por Mercado Pago.
74
+ - Copiar en Mercado Pago la **URL de notificaciones de Bacano** mostrada en la
75
+ configuración del método. La ruta identifica la conexión del comercio; no
76
+ debe inventarse ni compartirse entre configuraciones.
77
+ - Activar el evento **Order (Mercado Pago)** (`orders_v2`). No seleccionar
78
+ `Pagos (legacy)` como sustituto.
79
+ - Webhook, API pública y `returnUrl` deben ser HTTPS alcanzable. En local se
80
+ necesita un túnel; `localhost` no sirve para Mercado Pago.
81
+ - La Public Key no se utiliza en este modo de redirección alojada y nunca debe
82
+ colocarse en el campo Access Token.
83
+
84
+ No registrar, copiar a código, enviar al navegador ni incluir en capturas el
85
+ Access Token o la firma secreta. Si una credencial queda expuesta, rotarla.
86
+
87
+ ### Formato privado de Orders para Colombia
88
+
89
+ Este es el contrato del adaptador de Bacano validado contra la API colombiana
90
+ de Orders el 22 de septiembre de 2026. No debe implementarse en el storefront,
91
+ pero un agente que modifique el backend debe preservarlo y volver a probarlo si
92
+ Mercado Pago cambia su esquema.
93
+
94
+ ```json
95
+ {
96
+ "type": "online",
97
+ "processing_mode": "manual",
98
+ "total_amount": "19000",
99
+ "items": [
100
+ {
101
+ "title": "Pedido Bacano",
102
+ "quantity": 1,
103
+ "unit_price": "19000"
104
+ }
105
+ ],
106
+ "taxes": [{ "type": "IVA", "value": "0" }]
107
+ }
108
+ ```
109
+
110
+ Reglas que evitaron un rechazo real del proveedor:
111
+
112
+ - Los montos enviados a Orders son strings de pesos COP enteros: `"19000"`,
113
+ no `"19000.00"`. Bacano conserva centavos internamente y rechaza fracciones
114
+ que no pueda representar; nunca redondea silenciosamente.
115
+ - El ítem usa `title`, `quantity` y `unit_price`. En este contrato no enviar
116
+ `currency_id`, `total_amount` ni `unit_measure` dentro de `items[]`.
117
+ - `taxes[].value` es string, incluso cuando el IVA es cero. No enviarlo como
118
+ número.
119
+ - Orders rechaza `config.notification_url` en el payload de creación. La URL
120
+ se registra en **Webhooks** de la aplicación de Mercado Pago y no se repite
121
+ dentro de cada order.
122
+ - Si existe IVA, `unit_price × quantity + taxes` debe reconciliar exactamente
123
+ con `total_amount`. El adaptador utiliza el IVA calculado por Bacano; no
124
+ inventa una tarifa fija.
125
+ - El monto persistido, el total remoto autenticado y el pedido deben coincidir
126
+ antes de aprobar el pago.
127
+
128
+ La idempotencia de creación pertenece a Bacano. Tras una respuesta incierta se
129
+ reutiliza el mismo intento y la misma clave; generar una nueva order podría
130
+ producir un cobro duplicado. Un rechazo definitivo de creación (`400`, `401` o
131
+ `403`) cierra esa sesión y permite corregir la configuración sin considerar el
132
+ pedido pagado.
133
+
134
+ ### Diagnóstico rápido
135
+
136
+ | Síntoma | Interpretación inicial | Acción segura |
137
+ | --- | --- | --- |
138
+ | `MERCADOPAGO_ACCOUNT_ENVIRONMENT_MISMATCH` | La credencial y el ambiente no corresponden | Verificar credencial de prueba/productiva y vendedor; no cambiar el ambiente para ocultarlo |
139
+ | `MERCADOPAGO_HTTP_401` | Access Token inválido, revocado o mal copiado | Rotar/verificar el Access Token de la aplicación |
140
+ | `MERCADOPAGO_HTTP_403` al crear | La cuenta autentica, pero la aplicación no está habilitada para Orders o la operación | Confirmar API de Orders y capacidades de la aplicación |
141
+ | `MERCADOPAGO_HTTP_400` al crear | El proveedor rechazó el payload | Inspeccionar una versión sanitizada del error; nunca imprimir token ni PII |
142
+ | `MERCADOPAGO_RETURN_HTTPS_REQUIRED` | La URL de retorno no es HTTPS pública | Usar dominio publicado o túnel local |
143
+ | `MERCADOPAGO_CHECKOUT_PENDING_RECONCILIATION` | El resultado de creación es incierto | Reintentar la sesión existente; no crear otro pedido |
144
+
145
+ Un `403` que cambia a `400` después de crear una aplicación de Orders indica
146
+ que la autorización avanzó y ahora debe corregirse el contrato de la solicitud;
147
+ no significa que el Access Token sea la firma del webhook.
148
+
149
+ ## Openpay by BBVA · contrato vigente
150
+
151
+ Bacano usa el **Webcheckout alojado sin ID de cliente** de Openpay Colombia.
152
+ El storefront no carga Openpay.js ni recibe llaves: crea una sesión Bacano y
153
+ abre el `redirectUrl` validado que devuelve el backend.
154
+
155
+ ### Configuración del comercio
156
+
157
+ - Registrar en Bacano el **ID del comercio** y la **llave privada** del mismo
158
+ ambiente. La llave pública no participa en Webcheckout server-to-server.
159
+ - Sandbox usa `https://sandbox-api.openpay.co`; producción usa
160
+ `https://api.openpay.co`. Bacano fija esos orígenes y no acepta una URL de API
161
+ suministrada por el comercio.
162
+ - La API pública de Bacano debe ser HTTPS. Al guardar, Bacano registra el
163
+ webhook en Openpay con credenciales Basic generadas internamente y completa
164
+ la verificación que Openpay envía al endpoint. No se copian esas credenciales
165
+ al storefront ni se solicitan al usuario.
166
+ - El método queda guardado aunque falle la provisión remota del webhook; la
167
+ interfaz reporta el error y permite reintentar la misma conexión. No se
168
+ ofrece en el checkout hasta que el webhook figure verificado.
169
+
170
+ Referencia oficial: [API Openpay Colombia](https://documents.openpay.co/api/index.html).
171
+
172
+ ### Creación, recuperación y confirmación
173
+
174
+ - `POST /v1/{MERCHANT_ID}/checkouts` recibe monto entero COP, `iva` string,
175
+ referencia única, comprador, retorno HTTPS y una expiración anterior a la
176
+ expiración local de la sesión.
177
+ - Bacano persiste primero sesión, payload y snapshot cifrado de credenciales.
178
+ El primer intento crea directamente el checkout. Tras un timeout, primero
179
+ busca la misma referencia; si Openpay responde duplicado `1006`, recupera el
180
+ checkout existente. Nunca genera otra referencia para un reintento técnico.
181
+ - Solo se redirige a un link HTTPS del host exacto de Openpay y ruta `/ck/...`.
182
+ - El retorno y el webhook son señales para conciliar. Bacano consulta cargos
183
+ con la llave privada y exige igualdad de referencia, moneda e importe antes
184
+ de aprobar. Campos adicionales del webhook o del navegador no se registran
185
+ como evidencia financiera.
186
+ - Un cargo `failed` o `cancelled` no cierra por sí solo la sesión mientras el
187
+ checkout alojado siga disponible: el comprador puede probar otro medio. Un
188
+ cargo posterior `completed` aprueba si es el único aprobado y todos los demás
189
+ cargos son fallidos o cancelados. Cargos simultáneos pendientes, más de un
190
+ aprobado, reembolsos, contracargos o discrepancias pasan a revisión manual.
191
+ - El retorno al sitio no declara vencida una sesión remota por la hora local;
192
+ solo permite reintentar cuando el proveedor confirma el cierre del checkout.
193
+
194
+ El comprador usa los medios habilitados por Openpay dentro de su página
195
+ alojada; el SDK no pide tarjeta, PSE ni efectivo de forma separada.
196
+
197
+ ### Diagnóstico rápido
198
+
199
+ | Síntoma | Acción segura |
200
+ | --- | --- |
201
+ | `OPENPAY_CREDENTIALS_REQUIRED` | Ingresar la llave privada; no la llave pública |
202
+ | `OPENPAY_MERCHANT_ID_REQUIRED` | Copiar el ID del comercio del mismo ambiente |
203
+ | `OPENPAY_HTTP_401` / `OPENPAY_HTTP_403` | Verificar pareja comercio/llave y sandbox/producción |
204
+ | `OPENPAY_PUBLIC_HTTPS_REQUIRED` | Publicar la API Bacano por HTTPS antes de registrar el webhook |
205
+ | `OPENPAY_CHECKOUT_PENDING_RECONCILIATION` | Reutilizar la sesión y consultar; no crear otro pedido |
206
+ | `OPENPAY_*_REVIEW_REQUIRED` | No aprobar ni reintentar con otra referencia; revisar la evidencia |
207
+
208
+ ## Reglas específicas visibles
209
+
210
+ - Addi puede exigir CC y datos de contacto/residencia. Mostrar esos campos solo
211
+ cuando el método retornado tenga `gatewayCode === "ADDI"`; la evaluación de
212
+ crédito ocurre en Addi.
213
+ - El storefront debe usar el texto del modo seleccionado: para `pay_now`, el
214
+ botón principal comunica **Pagar ahora**, no solo **Confirmar pedido**.
215
+ - Recargos, impuestos, descuentos, envío y total final siempre vienen de
216
+ Bacano. El storefront no los recalcula ni altera el monto de la pasarela.
217
+ - Los secretos vacíos al editar una conexión significan conservar los
218
+ existentes; nunca intentar precargarlos en el navegador.
219
+
220
+ ## Prueba mínima antes de publicar
221
+
222
+ - Ejecutar un pago sandbox completo por cada pasarela activa, no solo validar
223
+ el formulario de configuración.
224
+ - Confirmar redirección inmediata al checkout alojado.
225
+ - Probar aprobación, rechazo, retorno, refresco, doble clic y reintento.
226
+ - Confirmar que el webhook responde, que Bacano consulta el recurso autenticado
227
+ y que repetir el evento no duplica pago, pedido, inventario ni conversión.
228
+ - Verificar el pedido desde `orders.getPublicOrder()` o
229
+ `orders.waitForPayment()`; no desde parámetros de retorno.
230
+ - Si el flujo real no se ejecutó, reportarlo como **NO VERIFICADO**, nunca como
231
+ listo para producción.
package/client.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { type FlowsClient } from "./flows";
1
2
  import type { BacanoClientConfig, BacanoClientContext, Brand, Cart, CartItem, CatalogAttributeList, CatalogCategoryList, CatalogPageResult, CheckoutResult, CheckoutSubmission, ContentEntry, CustomerAddress, CustomerProfile, CustomerProfileUpdate, GetAttributeListOptions, GetCatalogPageOptions, GetCategoryListOptions, GetContentOptions, GetOrdersOptions, GetProductFiltersOptions, GetProductRecommendationsOptions, GetProductSeoEntriesOptions, GetProductsOptions, GetSearchSuggestionsOptions, GetStaticCatalogBootstrapOptions, GetStaticCatalogSnapshotOptions, Order, Product, ProductAvailability, ProductCategory, ProductContentBlock, ProductFilters, ProductRecommendationsResult, ProductSeoEntriesResult, ProductVariantLiveState, PublicOrder, PublicOrderLookupInput, SearchSuggestion, StartGuestCheckoutInput, StaticCatalogBootstrap, StaticCatalogSnapshot, WaitForPublicOrderPaymentInput, WebsiteCampaignCaptureInput, WebsiteCampaignCaptureResult, WebsiteCheckoutAddress, WebsiteCheckoutItem, WebsiteCouponActionResult, WebsiteDeliveryOption, WebsiteFormResponseInput, WebsiteFormResponseResult, WebsiteFreeShippingProgress, WebsiteFulfillmentFamily, WebsiteGuestContact, WebsiteLocationOptions, WebsitePaymentMethod, WebsitePaymentOption, WebsitePaymentSessionResult, WebsitePublishedForm, WebsiteShippingPolicy, WebsiteShippingQuote } from "./types";
2
3
  export interface BacanoClient {
4
+ flows: FlowsClient;
3
5
  /** Initialize the client and resolve its website context. */
4
6
  init(): Promise<void>;
5
7
  /** Get the resolved website context (company_id, chain_id, etc.) */
@@ -89,7 +91,12 @@ export interface BacanoClient {
89
91
  orderId: string;
90
92
  publicOrderToken: string;
91
93
  paymentSessionId: string;
92
- gatewayTransactionId: string;
94
+ /**
95
+ * Provider transaction returned to the browser. Required by providers
96
+ * such as Wompi; omit it for Mercado Pago Orders and Addi, which Bacano
97
+ * reconciles server-side from the stored payment session.
98
+ */
99
+ gatewayTransactionId?: string;
93
100
  }): Promise<WebsitePaymentSessionResult>;
94
101
  };
95
102
  orders: {