# website_membership_signup_required

Módulo in-house (Criptomart) para el cliente **EMES** que replica el flujo de
alta de asociados de YourMembership (YM) dentro de Odoo 18.

## Qué hace

Intercepta el **"Add to Cart"** del e-commerce **solo para productos de
membresía** (`product.template.membership = True`). Si el visitante es
anónimo (usuario público) y el ajuste `membership_signup_required` del
website está activo (por defecto ON), el request no se completa y se redirige
al formulario estándar de `/web/signup` (de `auth_signup`), con un
`redirect` de vuelta a la ficha del producto y un marcador efímero
`membership_signup=1` que re-habilita el signup de modo condicional aunque el
website esté en modo **b2b** (*guest checkout*).

Tras crear la cuenta, el usuario queda autenticado y vuelve a la ficha del
producto, donde ya puede añadirlo al carrito y continuar al checkout.

Para el resto del catálogo (productos no miembros) el flujo **b2b nativo**
(*guest checkout*) se mantiene intacto, sin interferencia.

## Por qué es necesario un módulo propio

`website_sale` fuerza por defecto `website.auth_signup_uninvited = 'b2c'`
(signup/registro dentro del checkout). Pasar el website a **b2b** — lo que
permite *guest checkout* para el catálogo general — **deshabilita el route
`/web/signup`** para el público general. Para re-abrirlo **solo** durante el
flujo de membresía, este módulo extiende con un override conservador de
`res.users._get_signup_invitation_scope()` que devuelve `'b2c'` únicamente
cuando el request porta el marcador efímero (`membership_signup=1` en la query
o `membership_signup_active` en sesión). El marcador se setea al interceptar
el `cart_update` y se limpia tras `signup`/`login` exitosos.

## Características / requisitos funcionales

- **RF-01** Interceptación de `/shop/cart/update` y `/shop/cart/update_json`
  solo para `product.template.membership = True` y usuario público.
- **RF-02** Redirección a `/web/signup?membership_signup=1&redirect=/shop/<slug>`.
- **RF-03** Re-habilitación condicional del signup en b2b vía override de
  `_get_signup_invitation_scope`.
- **RF-04** Flujo completo: signup → vuelta a la ficha del producto →
  add to cart → carrito.
- **RF-05** Productos no miembros: sin interceptación, flujo b2b normal.
- **RF-06** Usuarios ya autenticados no se interceptan.
- **RF-07** Configurable por website desde Ajustes → E-commerce → "Membership
  signup required" (default ON).
- **RF-08** No afecta al backend ni a métodos de pago: la interceptación
  vive solo en los controllers `/shop/cart/update*`.
- **RF-09** Separación `firstname` / `lastname` en el formulario de signup
  (gracias a la dependencia OCA `partner_firstname`).
- **RF-10** El enlace "Already have an account?" del formulario de signup
  preserva el `redirect`, por lo que un usuario con cuenta existente puede
  loguearse y volver a la ficha del producto.
- **RF-11** i18n: `module.pot` + `es.po` (español dominante) + `gl.po` y
  `ca.po` como esqueleto.

## Dependencias

- `website_sale`
- `auth_signup`
- `membership`
- `partner_firstname` (OCA, `partner-contact/`).

## Configuración

1. Settings → Website → Ajustes e-commerce → "Customer Accounts" debe estar
   en **Optional (guest checkout)** o lo equivalente a `b2b` en el website
   activo (esto es `website.account_on_checkout = 'disabled'`).
2. Settings → "Membership signup required" checkbox: activado por defecto.
3. Para productos con `membership = True`, el flujo aplica; para el resto,
   no.

## Cómo correr los tests

```bash
# En el entorno EMES (desde OCB):
./odoo-bin -c emes.conf -i website_membership_signup_required \
    --test-tags website_membership_signup_required --stop-after-init
```

Los tests unitarios (TransactionCase) cubren RF-01..RF-08 con lógica; el tour
HttpCase end-to-end cubre RF-04 (flujo completo).

## Notas técnicas

- El override de `_get_signup_invitation_scope` es el punto más sensible de
  seguridad: solo devuelve `'b2c'` cuando el request porta el marcador
  efímero. No re-abre el signup libre en b2b para el catálogo general.
- La decisión de interceptar está centralizada en
  `website._membership_signup_required_for(product_id, is_public)` (capa
  modelo) para poder testearla sin HTTP y reutilizarla en futuras
  extensiones.
- El controller `_membership_signup_is_required(product_id)` delega en ese
  helper.
- Tras signup / login exitoso, se limpian los flags `membership_signup_active`
  y `membership_signup_product_id` de la sesión.

## Licencia

AGPL-3.

## Autor

Criptomart (<https://git.criptomart.net/criptomart/addons-cm>).

## Limitaciones / notas de seguridad

El marcador `membership_signup=1` que habilita el route `/web/signup` en modo
b2b es un **query param público**: cualquier visitante que conozca ese param
puede acceder al formulario de signup directamente, sin pasar por el carrito
de membresía. Esta **decisión está aceptada por diseño**: el signup crea
cuentas de tipo *portal* (no internal), y la postura b2b del catálogo general
se mantiene — el route solo se reabre de forma condicional vía el override de
`_get_signup_invitation_scope` cuando el request porta el marcador efímero (ver
`models/res_users.py` y `controllers/website_sale.py`); nunca se reabre el
signup libre para el resto del tráfico.

Para reforzar la seguridad se podría sustituir el query param estático por un
token HMAC firmado (vinculado a la sesión y al producto objetivo), de modo
que solo el flujo de `cart_update` pueda generar un redirect válido y el
acceso directo a `/web/signup` fuera de ese flujo quede restringido. Ver
`requirements.md` §7 R-01.