> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallet passes (Apple y Google)

> Cuando esto se entregue, un member agregará su tarjeta de fidelización a la billetera de su teléfono desde un enlace en un correo —sin descargar una app— y se actualizará sola cuando cambie su saldo.

> Traducción. Autoritativo: [`fs-loy-0014-wallet-passes.md`](/modules/loyalty/features/fs-loy-0014-wallet-passes).

## Contexto

Un wallet pass es la tarjeta de fidelización dentro de Apple Wallet o Google Wallet: saldo, nivel y un
código escaneable en el punto de venta. Elimina la mayor fricción de toda la categoría —"descárgate
nuestra app"— y la reemplaza por un enlace.

Para Latinoamérica importa más que en otras partes. Android domina el parque de dispositivos y Google
Wallet viene preinstalado, así que la cobertura es prácticamente universal, y el pass se actualiza por
push sin ninguna app nuestra en el dispositivo.

ADR-020 lo ubica como la capa intermedia de la estrategia mobile. Una cosa que quedó mal ahí y se
corrige acá: **los wallet passes se emiten siempre bajo las cuentas propias de Softcrum, nunca las
del cliente.** La app necesita la cuenta de desarrollador del cliente porque compite por un listing
en la tienda y la guideline 4.3 de Apple la empuja ahí; un pass no tiene listing, y su branding
—logo, colores, nombre de la organización— viaja dentro del propio archivo. Un tenant que quiere
algo que salga completamente bajo su nombre recibe la app white-label, que es justamente para eso.
Ofrecer un tier de wallet con cuenta del cliente sería vender lo mismo dos veces y sumar gestión de
certificados por cada tenant.

Eso convierte un spike en **bloqueante y no en modelador de tiers**: si Apple exige un Pass Type ID
por marca, emitir bajo nuestra cuenta podría no ser posible y la feature cambia de forma. Se
responde antes de escribir el ADR.

## Alcance *(normativo)*

* Generación de passes para Apple Wallet (`.pkpass`, firmado) y Google Wallet (objetos vía JWT).
* Branding por tenant guiado por la misma configuración de theming que la app mobile.
* Saldo, nivel y un código escaneable de member en la cara del pass.
* Actualizaciones push al cambiar saldo o nivel, por la cascada de notificaciones existente.
* `loyalty.wallet_pass_registrations` registrando qué dispositivos tienen qué pass.
* Un enlace de agregar-a-billetera firmado y con expiración, apto para embeber en un correo.

## Fuera de alcance *(normativo)*

* Pagos. Estos son passes de fidelización, nunca instrumentos de pago.
* Relevancia en pantalla de bloqueo por ubicación en la primera versión. Es una de las capacidades más
  potentes del formato, pero necesita coordenadas de local por tenant, que es su propio problema de
  datos.
* Validación offline en el punto de venta. El código se escanea y se valida contra nuestra API.

## Comportamiento *(normativo)*

1. Los passes se emiten bajo las **cuentas propias de Apple y Google de Softcrum, siempre**. Los
   certificados del cliente no se ofrecen: un tenant que quiere su propio canal recibe la app
   white-label (ADR-020). Las llaves de firma se guardan como secretos y se rotan sin reemitir cada
   pass; una llave filtrada que no se puede rotar es una reemisión completa a todos los members.
2. El enlace de agregar-a-billetera es firmado y **expira**. Un enlace que nunca expira es una
   credencial permanente sentada en una bandeja de correo.
3. Las actualizaciones del pass pasan por `NotificationChannelPort` como cualquier otro canal
   (ADR-019), con su propio adaptador. Saltarse la cascada aquí crearía un segundo camino de entrega
   sin auditoría.
4. Los push de actualización se **agrupan**: varios cambios de saldo dentro de una ventana corta
   producen un solo push. Un member cuyo teléfono vibra con cada punto acreditado va a eliminar el
   pass.
5. Un member puede tener el pass en varios dispositivos. Todos los dispositivos registrados reciben
   actualizaciones.
6. Desregistrar un dispositivo detiene sus actualizaciones y nunca afecta a los demás.
7. El código escaneable es un identificador de member **inútil sin autenticación** — escanearlo no
   otorga nada por sí solo. Es un identificador, no un token al portador.
8. Las actualizaciones del pass son de categoría `product`, no `marketing`: son un cambio de estado
   sobre algo que el member instaló deliberadamente.

## Datos *(normativo)*

| Tabla                               | Invariantes clave                                                                                                 |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `loyalty.wallet_passes`             | único por (programa, contacto, plataforma); `serial_number`; `auth_token` hasheado; `last_pushed_at`; `is_active` |
| `loyalty.wallet_pass_registrations` | FK pass; `device_id`, `push_token`; `registered_at`, `unregistered_at` nullable; append-only                      |

## API *(normativo)*

| Endpoint                                  | Clase   | Permiso                    | Presupuesto                       |
| ----------------------------------------- | ------- | -------------------------- | --------------------------------- |
| `POST /v1/loyalty/wallet-passes`          | Runtime | token de member            | p95 \<1 s (generar es más pesado) |
| `GET /v1/loyalty/wallet-passes/{serial}`  | Runtime | token de auth del pass     | p95 \<300 ms                      |
| Callbacks de servicio web de Apple/Google | Runtime | protocolo de la plataforma | según spec de cada plataforma     |

Los callbacks de plataforma siguen los protocolos propios de Apple y Google, que no nos toca diseñar.
Son el único lugar del módulo donde la forma del camino la dicta un tercero.

## Eventos *(normativo)*

`loyalty.wallet_pass.issued`. Los cambios de saldo y nivel se consumen de los eventos existentes en vez
de generar nuevos.

## Criterios de aceptación *(normativo)*

1. Un `.pkpass` generado valida contra los requisitos de firma de Apple e instala en un dispositivo.
2. Un objeto de Google Wallet instala desde el mismo enlace en Android.
3. Un cambio de saldo empuja una actualización dentro de 60 s a cada dispositivo registrado.
4. Diez cambios de saldo dentro de la ventana de agrupación producen exactamente un push.
5. La rotación de llaves emite passes válidos sin invalidar los ya instalados.
6. Un enlace de agregar-a-billetera vencido se rechaza con un error tipado.
7. **Negativo:** el código escaneable por sí solo no otorga acceso a ningún endpoint — probado por un
   test que lo intenta sin autenticación.

## Ejecución

Bloqueado por el ADR de librerías de firma (DEC-H5). Pipeline asíncrono: la generación y los push
corren en `backend/workers`; el adaptador de actualización de wallet es una implementación de
`NotificationChannelPort`.

## Preguntas abiertas

Ninguna pendiente. Toda pregunta que llevaba este spec quedó respondida en el registro consolidado
([`../../../../design/open-questions-v1.md`](/design/open-questions-v1), v1.1) y se
incorporó a las secciones normativas de arriba.

Sigue bloqueado, pero no por una decisión:

| # | Ítem                                                                                                          | Naturaleza                             | Estado                                                                                                            |
| - | ------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| 1 | **Spike, ahora bloqueante:** ¿un solo Pass Type ID de Apple puede servir a todo tenant con branding por pass? | Investigación                          | hay que responderlo primero — si no, emitir bajo nuestra cuenta podría ser imposible y la feature cambia de forma |
| 2 | Librerías de firma y ciclo de vida de certificados en ambas plataformas                                       | Necesita **ADR-023**, no una respuesta | este FS queda en `draft` hasta que exista                                                                         |

## Changelog

| Versión | Fecha      | Cambio                                                                             | Por qué                                    | Autor                  |
| ------- | ---------- | ---------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------- |
| 0.2.0   | 2026-08-17 | Preguntas abiertas resueltas (OQ-LOY-\*) e incorporadas a las secciones normativas | El owner respondió el registro consolidado | daniel + claude-opus-5 |
| 0.1.0   | 2026-08-17 | Borrador inicial                                                                   | —                                          | daniel + claude-opus-5 |

## Registro de entrega

*Aún no implementado.*
