Transferencia bancaria con Lit

Simulación de una transferencia bancaria con LitElement

Una interfaz puede representar la captura y el resultado de una transferencia sin conectarse a un banco. En esta guía construiremos una demostración local con LitElement para practicar propiedades reactivas, eventos, validaciones y plantillas. El componente no mueve dinero, no consulta cuentas reales y no confirma que un pago haya llegado a su destino.

1. Qué ocurre en una transferencia real

Una transferencia es una orden para enviar fondos desde una cuenta de origen a una cuenta beneficiaria. De forma simplificada, el cliente captura los datos y autoriza la operación; la institución emisora valida y tramita la orden; el sistema de pagos la encamina a la institución receptora; y el resultado depende de las confirmaciones de los sistemas involucrados.

Una pantalla que muestra «enviado» no demuestra por sí sola que el beneficiario recibió el dinero. En una aplicación real, el servidor debe validar la solicitud, aplicar controles de autorización y riesgo, registrar la operación y comunicar un estado respaldado por sus sistemas. La interfaz de este artículo solo ilustra el recorrido visual.

2. Qué aporta LitElement

LitElement es una clase base del proyecto Lit para crear elementos HTML personalizados. En este ejemplo:

  • static properties declara datos reactivos. Cuando cambian, Lit actualiza la plantilla.
  • html permite describir el marcado y conectar valores, condiciones y eventos.
  • static styles define estilos encapsulados para el componente.
  • render() devuelve la interfaz que corresponde al estado actual.
  • customElements.define() registra la etiqueta que se utilizará en HTML.

El componente se llama <transferencia-bancaria>. El nombre incluye un guion, como requieren los elementos personalizados del navegador.

3. Preparar el proyecto

Se necesita Node.js y npm. Una manera de crear una aplicación de prueba con Vite es:

npm create vite@latest demo-transferencia -- --template vanilla
cd demo-transferencia
npm install
npm install lit
npm run dev

En la plantilla de Vite, index.html carga src/main.js. Dentro de <body>, agrega el elemento:

<transferencia-bancaria></transferencia-bancaria>

4. Crear el componente

Sustituye el contenido de src/main.js por este código:

import { LitElement, html, css } from "lit";

class TransferenciaBancaria extends LitElement {
  static properties = {
    beneficiario: { type: String },
    cuentaDestino: { type: String },
    importe: { type: String },
    saldoCentavos: { type: Number },
    estado: { type: String },
    comprobante: { type: String },
    error: { type: String }
  };

  static styles = css`
    :host {
      display: block;
      max-width: 620px;
      margin: 24px auto;
      padding: 24px;
      color: #172033;
      font-family: Arial, sans-serif;
      border: 1px solid #d8deea;
      border-radius: 12px;
      background: #ffffff;
    }

    h1 { margin-top: 0; font-size: 1.5rem; }
    label { display: block; margin-top: 14px; font-weight: 600; }
    input {
      box-sizing: border-box;
      width: 100%;
      margin-top: 6px;
      padding: 10px;
      border: 1px solid #9aa6ba;
      border-radius: 6px;
      font: inherit;
    }
    button {
      margin-top: 18px;
      padding: 10px 14px;
      border: 0;
      border-radius: 6px;
      background: #174ea6;
      color: #ffffff;
      font: inherit;
      cursor: pointer;
    }
    button:disabled { opacity: 0.55; cursor: not-allowed; }
    .saldo { font-weight: 700; }
    .error { color: #a21d2b; }
    .resultado {
      margin-top: 18px;
      padding: 12px;
      border-radius: 6px;
      background: #f0f5ff;
      overflow-wrap: anywhere;
    }
    .nota { color: #46536a; font-size: 0.9rem; }
  `;

  constructor() {
    super();
    this.beneficiario = "";
    this.cuentaDestino = "";
    this.importe = "";
    this.saldoCentavos = 500000;
    this.estado = "Sin procesar";
    this.comprobante = "";
    this.error = "";
  }

  actualizarCampo(evento) {
    const { name, value } = evento.target;
    this[name] = value;
    this.error = "";
    this.estado = "Sin procesar";
    this.comprobante = "";
  }

  procesarTransferencia() {
    this.error = "";
    this.comprobante = "";

    const importeValido = /^\d+(?:\.\d{1,2})?$/.test(this.importe);
    const importeCentavos = importeValido
      ? Math.round(Number(this.importe) * 100)
      : 0;

    if (this.beneficiario.trim().length < 2) {
      this.error = "Escribe un nombre de beneficiario de al menos 2 caracteres.";
    } else if (!/^\d{10,18}$/.test(this.cuentaDestino)) {
      this.error = "La cuenta de prueba debe tener entre 10 y 18 dígitos.";
    } else if (!Number.isSafeInteger(importeCentavos) || importeCentavos <= 0) {
      this.error = "El importe debe ser mayor que cero y tener hasta 2 decimales.";
    } else if (importeCentavos > this.saldoCentavos) {
      this.error = "El saldo simulado no es suficiente.";
    }

    if (this.error) {
      this.estado = "Rechazada en la simulación";
      return;
    }

    this.saldoCentavos -= importeCentavos;
    this.estado = "Simulación completada";
    this.comprobante = `DEMO-${Date.now()}`;
  }

  formatearMoneda(centavos) {
    return new Intl.NumberFormat("es-MX", {
      style: "currency",
      currency: "MXN"
    }).format(centavos / 100);
  }

  render() {
    return html`
      <h1>Transferencia bancaria (demo)</h1>
      <p class="nota">
        Simulación local. No se envía dinero ni se contacta a un banco.
      </p>

      <p>Saldo disponible en la simulación:</p>
      <p class="saldo">${this.formatearMoneda(this.saldoCentavos)}</p>

      <label for="beneficiario">Nombre del beneficiario</label>
      <input
        id="beneficiario"
        name="beneficiario"
        autocomplete="off"
        .value=${this.beneficiario}
        @input=${this.actualizarCampo}
      />

      <label for="cuentaDestino">Cuenta ficticia (10 a 18 dígitos)</label>
      <input
        id="cuentaDestino"
        name="cuentaDestino"
        inputmode="numeric"
        autocomplete="off"
        .value=${this.cuentaDestino}
        @input=${this.actualizarCampo}
      />

      <label for="importe">Importe en MXN</label>
      <input
        id="importe"
        name="importe"
        type="number"
        min="0.01"
        step="0.01"
        .value=${this.importe}
        @input=${this.actualizarCampo}
      />

      <button
        @click=${this.procesarTransferencia}
        ?disabled=${this.estado === "Simulación completada"}
      >
        Simular transferencia
      </button>

      ${this.error
        ? html`<p class="error" role="alert">${this.error}</p>`
        : ""}

      <section class="resultado" aria-live="polite">
        <p><strong>Estado:</strong> ${this.estado}</p>
        ${this.comprobante
          ? html`
              <p><strong>Comprobante de demostración:</strong>
                ${this.comprobante}</p>
              <p><strong>Importe simulado:</strong>
                ${this.formatearMoneda(Math.round(Number(this.importe) * 100))}</p>
              <p><strong>Beneficiario:</strong> ${this.beneficiario}</p>
              <p class="nota">
                Este identificador se genera en el navegador y no es un
                comprobante bancario válido.
              </p>
            `
          : ""}
      </section>
    `;
  }
}

customElements.define("transferencia-bancaria", TransferenciaBancaria);

El saldo se guarda como un entero de centavos para evitar hacer las operaciones de esta demostración con decimales binarios. Esto no sustituye el diseño monetario ni las reglas contables de un sistema real.

5. Cómo funciona el estado reactivo

El constructor inicia el saldo simulado en 500 000 centavos, equivalentes a $5,000.00 MXN. beneficiario, cuentaDestino e importe comienzan como cadenas vacías. Lit observa las propiedades declaradas y vuelve a actualizar la plantilla cuando el componente les asigna nuevos valores.

Los campos enlazan su propiedad .value con el estado, y el evento @input llama a actualizarCampo(). El atributo name identifica qué propiedad actualizar. Al modificar cualquier dato se borran el error y el comprobante anterior, porque el resultado previo ya no corresponde a la captura actual.

En render(), ${...} inserta valores en la plantilla, mientras que los bloques condicionales muestran el mensaje de error o los datos del resultado solo cuando existen. Lit trata las expresiones como texto en esos lugares. El área aria-live="polite" comunica los cambios de estado a tecnologías de asistencia y role="alert" identifica el error.

6. Validar y simular

procesarTransferencia() revisa los datos en orden:

  1. El nombre debe tener al menos dos caracteres después de quitar espacios en los extremos.
  2. La cuenta ficticia debe contener únicamente entre 10 y 18 dígitos.
  3. El importe debe ser positivo, tener como máximo dos decimales y convertirse a un entero seguro de centavos.
  4. El saldo simulado debe cubrir el importe.

La expresión /^\d{10,18}$/ significa que toda la cadena debe tener entre 10 y 18 dígitos. Es una regla para el ejercicio, no una validación universal de CLABE, tarjeta o cuenta bancaria. Si una comprobación falla, el componente muestra el error y no modifica el saldo.

Cuando las comprobaciones pasan, se descuentan los centavos del saldo local y se genera un identificador con prefijo DEMO-. Por ejemplo, con saldo de 5,000.00 MXN y una captura de 750.00 MXN, la pantalla muestra $4,250.00 MXN. Ese resultado solo existe en el navegador y el identificador no permite verificar una operación real.

7. Límites y controles de producción

Este ejercicio no se conecta a una API ni mueve dinero. La validación en el navegador ayuda a la interacción, pero no es una frontera de seguridad: una aplicación real debe repetir las comprobaciones en un backend autenticado y autorizado.

Un sistema de transferencias también necesita controles como:

  • Verificación de permisos, cuenta de destino, límites y fondos disponibles.
  • Manejo de solicitudes repetidas, por ejemplo, mediante claves de idempotencia.
  • Registro de auditoría, monitoreo, conciliación y estados que reflejen respuestas confirmadas.
  • Protección de datos personales y comunicaciones cifradas.
  • Cálculos monetarios y transacciones que respeten las reglas del dominio.

El comprobante debe provenir del sistema que procesa y confirma la operación, no de un valor generado en el navegador. Nunca se deben incluir contraseñas, NIP, tokens, claves privadas ni credenciales bancarias en el código del cliente. Tampoco se debe usar este ejemplo con cuentas o dinero reales.

Conclusión

LitElement permite construir una interfaz reutilizable con propiedades reactivas, plantillas declarativas y estilos encapsulados. La demostración recorre la captura, las validaciones locales y la presentación de un resultado ficticio. La diferencia esencial es que una interfaz puede representar una transferencia, pero solo los sistemas autorizados pueden validarla, procesarla y confirmar su recepción.

© Arturo Juárez Monroy