add website_membership_signup_required: force to create user before add to cart a membership product

This commit is contained in:
Luis 2026-07-07 10:27:13 +02:00
parent 80f24bcc46
commit c4d32c6add
22 changed files with 1202 additions and 0 deletions

View file

@ -0,0 +1,123 @@
# 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.