Rexvalentia

Markr (6): créditos y planes sin marear al usuario

7 min de lectura

En la entrada anterior dejé el coste de una corrección medido: unos 0,032 $ al momento y la mitad en lote. Esta entrada va de cómo convertir eso en un modelo de cobro. Es la parte del proyecto donde más veces cambié de opinión, y casi todos los cambios fueron en la misma dirección: simplificar.

El objetivo: que gane yo, pero sin marear a nadie

El punto de partida que me puse:

  • tiene que ser rentable, incluso en el peor caso;
  • el usuario tiene que saber antes de enviar cuánto le va a costar;
  • nada de cálculos raros, y nadie debe pagar de más.

Y una restricción que repetí varias veces: las optimizaciones de coste tenían que mejorar el trato para las dos partes, no solo mi margen.

Primer intento: créditos por palabras

El coste crece con la longitud del texto, así que la primera propuesta fue lógica desde el punto de vista del coste: 1 crédito = 500 palabras, en cuartos de crédito, y la mitad en lote.

Hice una prueba mental: un profesor con 60 créditos, ¿cuántos escritos puede corregir? La respuesta fue una tabla:

Longitud del escrito Créditos Escritos con 60 créditos
hasta 125 palabras 0,25 240
126-250 palabras 0,50 120
251-375 palabras 0,75 80
376-500 palabras 1,00 60

Justo y exacto, pero ningún profesor quiere hacer esa cuenta. Si para responder "¿cuántas redacciones puedo corregir?" hace falta una tabla, el sistema está mal.

La regla definitiva: una frase

1 escrito = 1 crédito. Si pasa de una página, 2. En lote, la mitad. Si falla, no se cobra.

Por qué funciona:

  • En la práctica es "1 crédito = 1 escrito". Ninguna redacción de examen de A1 a C2 llega a una página: la más larga, la de C2, tiene unas 320 palabras.
  • La excepción evita perder dinero. Un documento de 5 páginas cuesta unas 13 veces más que una redacción corta. Cobrado a 1 crédito, cada uno daría pérdidas.
  • Descarté el medio crédito para textos cortos. Uno de 80 palabras cuesta unos 0,020 € y uno de 300, unos 0,042 €. La diferencia son dos céntimos: no justifica un tramo más que explicar.
  • El coste se ve antes de enviar: "320 palabras · 1 crédito" o "620 palabras · 2 créditos (más de una página)".

Toda la regla vive en un solo módulo:

LONG_TEXT_WORDS = 500  # más de esto (una página) es un documento largo
BATCH_FACTOR = Decimal("0.5")


def credits_for(words: int, batch: bool = False) -> Decimal:
    credits = Decimal(2 if int(words) > LONG_TEXT_WORDS else 1)
    return credits * BATCH_FACTOR if batch else credits

Si algún día cambia la regla, cambia este fichero y nada más.

De siete planes a tres

La primera tabla de precios tenía cuatro packs y tres suscripciones. Mi reacción fue: "me parece una locura la cantidad de planes que has creado". Antes de eso, además, había pedido bajar los márgenes: prefería ganar menos por usuario y que el usuario recibiera más.

La solución final:

Tres planes mensuales

Plan Precio (IVA incl.) Escritos al mes Por escrito
Básico 9,99 € 100 0,10 €
Profesor 19,99 € 220 0,09 €
Centro 69,99 € 800 0,087 €

Compra libre, a partir de 25 créditos y con precio por volumen:

Créditos Precio por escrito
25 a 99 0,14 €
100 a 299 0,12 €
300 o más 0,10 €

La escala tiene que ser coherente: cada plan sale más barato por escrito que el anterior, y suscribirse siempre sale mejor que comprar esos mismos créditos sueltos. Si no, nadie tendría motivo para elegir cada opción. Y si comprar un poco más deja en un tramo más barato pagando lo mismo o menos, la pantalla avisa: "Por 0,60 € menos te llevas 100 créditos".

Con estos números, el margen típico queda entre el 38 % y el 57 %, y sigue siendo positivo en el peor caso: que alguien gaste todos sus créditos en textos de una página completa. Por encima de una página ya se cobran 2 créditos, así que no hay escenario con pérdidas.

Ningún crédito caduca

La propuesta inicial tenía reglas de caducidad: los escritos del plan que no gastas pasan al mes siguiente hasta el doble del plan, los comprados no caducan, y se gastan primero los del plan... Solo escribirlo ya cansa.

Al implementarlo cambió por una regla más simple y más justa: un único saldo que no caduca nunca. Los créditos del plan, los comprados y los de bienvenida van al mismo saldo, y solo corregir lo reduce.

Situación Qué pasa con tus créditos
Subir de plan Se mantienen y se suman los proporcionales al resto del mes
Bajar de plan Se mantienen; el cambio llega en la renovación
Cancelar o pasar a compra libre Se mantienen y se pueden seguir usando
Una corrección falla Se devuelven automáticamente

¿Y para mí? Cada crédito ya está cobrado con margen positivo, así que no caducar solo significa que el usuario acaba usando todo lo que pagó. Me parece la forma honesta de hacerlo.

Subir de plan es proporcional en dinero y en créditos. Si solo se cobrara la parte proporcional pero se entregaran todos los escritos de diferencia, alguien podría subir el último día del mes y llevárselos casi gratis. Si el cobro proporcional saliera por debajo de 0,50 €, la subida se programa para la renovación, porque un pago con tarjeta tan pequeño no compensa.

Un libro de movimientos

Por dentro, el saldo no es un número que se suma y se resta sin más. Toda modificación pasa por una única función, dentro de una transacción con la cuenta bloqueada (select_for_update), y deja un movimiento con su motivo y el saldo resultante. El usuario ve ese historial, y un test comprueba que la suma de los movimientos coincide siempre con el saldo.

Dos detalles más:

  • Se cobra al enviar, en la misma transacción que crea la corrección. Si no hay saldo, no se crea nada y se explica cuánto falta. Si después la corrección falla, se devuelve.
  • La confirmación de pagos es idempotente. Si el proveedor de pagos avisa dos veces del mismo pago, los créditos se suman una sola vez.

Pagos: preparados, pero sin Stripe

Stripe todavía no está conectado, y fue una decisión consciente. La capa de pagos tiene tres modos con MARKR_BILLING_PROVIDER:

  • simulated, para desarrollo y pruebas, con un botón que confirma el pago;
  • disabled, el de producción por ahora: los planes se ven y se pueden elegir, pero la compra aparece como "próximamente";
  • y el futuro Stripe, que solo necesitará la pantalla de pago y un webhook que confirme cada pago. El resto ya está.

Sin pagos nadie podría corregir nada, así que cada registro recibe 10 créditos de bienvenida, que además sirven para probar el producto. Cuestan menos de medio euro por usuario. Para soporte hay un comando que regala créditos dejando constancia en el historial:

python manage.py grant_credits <usuario> <créditos> --motivo "..."

Los planes aparecen en la landing, en el registro (se puede empezar solo con los créditos de bienvenida o elegir un plan) y en el menú, donde se ve el saldo y el plan ("270 créditos · Profesor"). Los términos de uso incluyen ya la renovación, los cambios de plan y el derecho de desistimiento de 14 días sobre los créditos no usados.

¿Sale a cuenta?

Pedí proyecciones con casos típicos. Con estos supuestos:

  • mezcla de usuarios: 50 % Básico, 20 % Profesor, 5 % Centro y 25 % compra libre;
  • los suscriptores usan el 70 % de su plan;
  • escrito típico de 300 palabras;
  • IVA, comisiones de Stripe y 1 $ = 0,90 €;

el resultado sale en torno a 6,60 € de beneficio por usuario y mes, bastante estable con cualquier volumen, porque el coste principal (la API) crece en proporción a los usuarios y el hosting pesa muy poco:

Usuarios Escritos al mes Facturado Beneficio al mes
25 ~2.500 350 € ~160 €
100 ~10.200 1.400 € ~660 €
500 ~51.000 7.000 € ~3.300 €

Si todos agotaran su plan cada mes, el beneficio bajaría un 30 % aproximadamente, pero seguiría siendo positivo. Lo que no incluye: impuestos sobre el beneficio, cuota de autónomo y captación de clientes. La cuota de autónomo, por cierto, se come casi todo el beneficio por debajo de unos 50-100 usuarios. Son números para tomar decisiones, no una promesa.

En la última entrada de la serie: el pulido final (modo noche, cuenta de demostración reproducible y exportación a PDF pensada para papel) y lo que aprendí construyendo todo esto en un día.

Esc

Pulsa Intro para buscar en todo el blog.