# Blueprint — Programa de descuentos para estudiantes (becas)

Feature de Humming: postulante de universidad → (aprobado) → código de descuento personalizado, con conexión y
vencimiento, reutilizable → en checkout se valida que **el pasajero que viaja es el becado**.

## Principio de diseño (replanteo 2026-07-12)

El voucher de beca **vive enteramente en el motor** (`fly.*`) y se ancla al **DOCUMENTO del becado**
(`IDNumber` + `IDCountry`, normalizado con `NORMALIZE_DOCUMENTS`), **NO a `usr_id`**.

**Por qué no `usr_id`**: `usr_id` es del **ERP** (`system.users`); `fly.vouchers`/`fly.passengers` son del **motor**
(migra a AWS Lambda). Atar el voucher a `usr_id` acoplaría los dos sistemas en el camino caliente del checkout (el
Lambda tendría que llamar a xGestion para emitir *y* redimir). **Por qué no `passengerId`**: es inestable (un mismo
humano tiene N filas de pasajero por duplicados/fantasmas/re-creación) — el mismo problema que originó el party model.

**El documento es la clave motor-local estable**: sobrevive a la re-creación del pasajero, es self-contained, es el
estándar aéreo (el viajero se identifica por su documento de viaje), y no necesita al ERP.

> El **centro de unicidad de personas** (`usr_id`, `system.identifiers`, `PERSON_*`, editor `users.php`) sigue vivo como
> capacidad **independiente del ERP** (CRM/WhatsApp/facturación). **Ya NO es dependencia de la beca.** El motor y el ERP
> quedan desacoplados: `fly.passengers` y `fly.vouchers` **no tienen columna `usr_id`**.

## Flujo

```
Postulación (form universidad)  →  fly.postulaciones (estado=pendiente)  [documento del becado]
        │
   Aprobación (operador)  →  emite voucher: { voucherCode, IDNumber, IDCountry, connection1/2, untilDate, VOUCHER_FLEXIBLE, %desc }
        │
   Checkout: pasajero ingresa voucherCode
        →  ¿ALGÚN pasajero del booking tiene el MISMO documento (normalizado) que el voucher?
              SÍ → aplica el % de descuento   |   NO → rechaza "el código es de <doc>, no está entre los pasajeros"
```

---

## CAPA 1 — Voucher personalizado por documento (descuento %)   [HECHO, salvo DDL]

**DDL** (aditivo; correr en prod):
```sql
ALTER TABLE fly.vouchers
  ADD COLUMN IDNumber  VARCHAR(32) NOT NULL DEFAULT '' AFTER acc_id,   -- documento del becado (normalizado)
  ADD COLUMN IDCountry VARCHAR(2)  NOT NULL DEFAULT '' AFTER IDNumber,  -- país emisor (ISO alfa-2)
  ADD COLUMN discount  DECIMAL(5,2) NOT NULL DEFAULT 0 AFTER punit,     -- % de descuento (0 = prepago clásico)
  ADD KEY idx_voucher_doc (IDNumber, IDCountry);
```
- Voucher de beca = tiene documento + `discount > 0`. Voucher de cuenta clásico = sin documento, usa `punit`.
- **`discount`** ya estaba (de la iteración previa). El pivot agrega `IDNumber`/`IDCountry`.

**Desacople — quitar `usr_id` del motor** (correr en prod):
```sql
ALTER TABLE fly.vouchers   DROP COLUMN usr_id;
ALTER TABLE fly.passengers DROP KEY idx_passenger_usr, DROP COLUMN usr_id;
```

**Código (hecho)**:
- `AL_ADD_NEW_VOUCHER` (airlines.php): normaliza (`NORMALIZE_DOCUMENTS`) y guarda `IDNumber`/`IDCountry`. Sin `usr_id`.
- `passenger_vouchers.php`: emite desde el **documento del pasajero**. Gate = "el pasajero tiene documento" (si no,
  manda a cargarlo en el editor del pasajero). Sin Capa "vincular persona", sin `usr_id`, sin `PERSON_*`.
- `motor.php execute=issueBecaVoucher` (Lambda al aprobar): recibe `IDNumber`/`IDCountry` (no `usr_id`), `acc_id` = 131
  (cuenta formal; la clave real es el documento), crea el voucher, devuelve `voucherCode` + documento.

---

## CAPA 2 — Postulaciones (`fly.postulaciones`)   [PENDIENTE]

**DDL** `fly.postulaciones`:
- `id, first_name, last_name, IDType, IDNumber, IDCountry, email, phn_number, university, connection1, connection2,
   status (pendiente/aprobado/rechazado), created_at, resolved_by, resolved_at`. **Sin `usr_id`** (motor-local).

**Formulario de carga** — **[D1 pendiente: extranet self-service de la universidad, o intranet operador?]**
- Al guardar: `NORMALIZE_DOCUMENTS` sobre el documento del postulante.

**Pre-registro del pasajero (motor)** — el Lambda llama `motor.php execute=getOrCreatePassenger` con el documento +
nombre → devuelve **passengerId** (get-or-create por documento via `AL_PASSENGER_REGISTER`). **Motor-local**: NO crea
persona del ERP ni devuelve `usr_id`. Así el Lambda tiene el `passengerId` del estudiante por adelantado.

**Pantalla de revisión (intranet)** — listar pendientes, aprobar/rechazar. Al **aprobar** → emite el voucher (Capa 1)
con el documento de la postulación.

---

## CAPA 3 — Checkout: descuento per-pasajero   [HECHO backend, falta UI de cierre]

**Estructura (decidida)**: el descuento se registra como una fila en **`fly.transactions`** con `fare_item = appliedDiscounts`
(bit 1024, per-reserva = per-pasajero). Esa fila es a la vez: la línea de descuento, el registro de redención/consumo, y la
integración a pricing/billing. **Cero DDL** (tabla y bit ya existían). Se descartó un ledger nuevo y el canal PROMO (booking-level).

**`AL_PASSENGER_VOUCHER_REDEEM(voucherId, bookingId)`** (airlines.php):
1. Valida voucher personal (`discount > 0` + `IDNumber`) + vigencia (`untilDate`) + consumo (bookings distintos con
   `appliedDiscounts ref=voucherId` vs `tripCount`).
2. Match por **documento normalizado**: reserva(s) del booking cuyo pasajero tiene el mismo `(IDNumber, IDCountry)` que el
   voucher. Si ninguno → `stxRejected` ("el código es de <doc>, no está entre los pasajeros").
3. Inserta la línea `appliedDiscounts` (amount NEGATIVO = `base_fare × discount%`) **solo en la(s) reserva(s) del becado**.
4. `AL_BILLING_UPSERT` para refrescar la factura. Idempotente.

**Integración de precio (3 sitios, todos hechos)**:
- `AL_BOOKING_PRICES` → ítem `Discounts` pre-tax (display + cobro).
- `AL_BILLING_UPSERT` → **parte la línea de vuelo** por punit efectivo (`base_fare + ifnull(disc,0)` per reserva) → el
  descuento del becado aparece en la factura sin tocar `base_fare` ni descontar al resto.
- `bookingModifyPrice` (reverse-eng) → resta `discountUSD` en el despeje de `base_fare`.
- Hallazgo: **billing NO pasa por `AL_PROMO_CALC`** (se puebla directo de `base_fare`), por eso hubo que tocar los 3.

**Motor**: acción `redeemPassengerVoucher` (acepta `voucherCode` o `voucherId` + `bookingId`).

> Match **por documento normalizado**, per-pasajero, dentro de `fly.*`. Ni `usr_id` ni `passengerId` como clave (el
> `passengerId` aparece solo indirecto, vía la reserva). **PENDIENTE**: el botón/campo en la pantalla de **cierre** para
> que el operador ingrese el código (backend + acción listos).

---

## Relación con el centro de personas (ERP) — SEPARADO

El centro de unicidad (`system.identifiers`, `PERSON_*`, editor `users.php`, preview) **no participa de la beca**.
Sigue evolucionando por su cuenta para CRM/WhatsApp/facturación. El único puente ERP↔motor es el **documento**
(mismo `NORMALIZE_DOCUMENTS` de los dos lados): el editor de personas resuelve "qué pasajeros son esta persona"
matcheando documentos contra `system.identifiers`, sin columna `usr_id` en `fly.passengers`.

---

## Orden de construcción

1. ~~**Capa 1** (voucher por documento + emisión)~~ — HECHO (falta correr el DDL).
2. **Capa 3** (chequeo checkout) — la lógica nueva de negocio.
3. **Capa 2** (postulaciones + form + aprobación) — la UI, lo más voluminoso.

## Decisiones

- ~~**Clave del voucher**~~ RESUELTO: **documento** (`IDNumber`+`IDCountry`), no `usr_id` ni `passengerId`.
- ~~**Descuento vs prepago**~~ RESUELTO: **% de descuento** (columna `discount`).
- **D1 (Capa 2)**: ¿el form de postulación es extranet (self-service universidad) o intranet (operador carga)?
- **Nota**: un pasajero SIN documento (fantasma J.Doe) no puede recibir beca (correcto: no se da beca a alguien no
  identificado). El flujo obliga a cargar el documento primero.
