Este tutorial adapta pedagógicamente el Día 97. La sección 📝 Contenido original del curso proviene del único archivo de la carpeta:
dia97-tarea16.md(enunciado y pregunta de reflexión, en inglés; aquí traducido). El curso no ofrece guía ni solución, y este tutorial no entrega la tienda terminada. Todo lo que no está en ese archivo (adaptación al contexto escolar con pagos de prueba, modelo de datos, carrito en la sesión, flujo de Stripe Checkout, seguridad de los pagos, retos 🟢🟡🟠🔴, proyecto extra y la etapa 16 de Mi Portafolio Profesional) es creación de esta adaptación.
Nota sobre las verificaciones: el 28 de septiembre de 2026, con Python 3.14 y Flask 3.1.3, se ejecutó la lógica del carrito de la sección 4 con el cliente de pruebas: agregar 2 + 1 camisetas y 1 termo dio un total de 133 000, calculado con los precios del servidor aunque el formulario enviara un campo
preciofalso; una cantidad negativa respondió 400; un producto inexistente, 404. Se comprobaron dos comportamientos sorprendentes que se explican en la sección 4:request.form.get("cantidad", 1, type=int)convierte"abc"en1sin error, y modificar un diccionario dentro desessionsin reasignarlo no se guarda. El código de Stripe de la sección 5 no se ejecutó (requiere una cuenta).
🏫 Para el aula: el enunciado pide "cobrar pagos reales". En este tutorial se trabaja solo en modo de prueba de la pasarela de pagos: ningún estudiante debe procesar dinero real. Cobrar pagos reales requiere ser mayor de edad, una cuenta verificada, obligaciones legales y tributarias, y políticas de devolución y protección de datos.
Tabla de contenidos
- La tarea: tu propia tienda en línea
- Analizar el problema: las piezas de una tienda
- 📖 Modelo de datos
- 📖 El carrito en la sesión (y dos trampas)
- 📖 El pago: Stripe Checkout en modo de prueba
- 🚀 Proyecto del día: construcción por versiones
- 🌟 Proyecto extra: tienda de la feria empresarial escolar
- 🏗️ Mi Portafolio Profesional — Etapa 16
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Observaciones sobre el material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 97
1. La tarea: tu propia tienda en línea
(Fuente: dia97-tarea16.md)
📝 Contenido original del curso
Usando lo que aprendiste al construir el blog con Flask, ahora construirás tu propio sitio de comercio electrónico (eCommerce). Tu sitio debe tener un carrito y un proceso de pago (checkout) que funcionen.
- Debe poder mostrar artículos a la venta y cobrar pagos reales de los usuarios.
- Debe tener funciones de autenticación: inicio de sesión y registro.
Un sitio de ejemplo: store.waitbutwhy.com.
Deberías considerar usar la API de Stripe (documentación de Stripe Checkout).
Pregunta de la tarea — Momento de reflexión: cómo abordaste el proyecto; qué fue difícil y qué fácil; cómo mejorarías; tu mayor aprendizaje; qué harías distinto.
Prerequisitos: el blog con Flask (Días 57, 59, 67 y 69); SQLAlchemy y relaciones entre tablas (Días 63 y 69); autenticación con Flask-Login y contraseñas con hash (Día 68); formularios con WTForms (Días 61–62); APIs y claves en variables de entorno (Días 33–35); publicar (Día 71).
2. Analizar el problema: las piezas de una tienda
(Sección creada para esta adaptación.)
🔍 Antes de programar
Visita una tienda en línea real y recorre el proceso completo sin comprar. Anota cada pantalla:
Catálogo ──► Detalle del producto ──► Carrito ──► ¿Sesión iniciada? ──no──► Login/Registro
│ sí
▼
Pago (página de la pasarela)
│ │
éxito cancelado
▼ ▼
Pedido confirmado Vuelve al carrito| Pieza | ¿Ya la sabes hacer? | Día de referencia |
|---|---|---|
| Catálogo y detalle desde la base de datos | Sí | 63–64 |
| Registro, login, contraseñas con hash | Sí | 68–69 |
| Carrito | Nuevo (sesión) | Sección 4 |
| Pago con una pasarela | Nuevo | Sección 5 |
| Pedidos del usuario | Relación entre tablas | 69 |
💡 La regla más importante de toda tienda: el servidor decide los precios. Todo lo que llega del navegador (precios, totales, descuentos) puede estar manipulado.
3. 📖 Modelo de datos
(Sección creada para esta adaptación.)
Usuario (id, email, contraseña_hash, nombre)
│ 1
│ tiene muchos
▼ *
Pedido (id, usuario_id, fecha, estado, total, id_sesion_pago)
│ 1
│ tiene muchos
▼ *
LineaPedido (id, pedido_id, producto_id, cantidad, precio_unitario)
│ *
▼ 1
Producto (id, nombre, descripción, precio, imagen, existencias)Preguntas de diseño (respóndelas en tu diario):
- ¿Por qué
LineaPedidoguardaprecio_unitariosi el producto ya tiene precio? (Pista: ¿qué pasa con los pedidos antiguos si mañana cambias el precio?) - ¿Por qué el
estadodel pedido ("pendiente","pagado","cancelado") es necesario si ya hay una página de éxito? - Precios como enteros: guarda el precio en la unidad mínima (pesos, o centavos para dólares) como entero, no como
float. Losfloatacumulan errores de redondeo (prueba0.1 + 0.2en Python).
4. 📖 El carrito en la sesión (y dos trampas)
(Sección creada para esta adaptación. Verificado con el cliente de pruebas de Flask.)
🎯 Objetivo de aprendizaje
Guardar un carrito entre páginas sin base de datos, con precios calculados en el servidor.
📖 Explicación conceptual
La sesión de Flask (session) es un diccionario que se guarda firmado en una cookie del navegador: el usuario puede leerla (no está cifrada) pero no modificarla sin que Flask lo detecte, gracias a la secret_key. Es ideal para un carrito pequeño: {"id_producto": cantidad}.
Como el contenido se puede leer, no guardes datos sensibles en la sesión. Y la
secret_keydebe ser larga, aleatoria y estar en una variable de entorno.
🔎 Ejemplo explicado
PRODUCTOS = {
1: {"nombre": "Camiseta del club de robótica", "precio": 35000},
2: {"nombre": "Termo reutilizable", "precio": 28000},
}
def total_carrito(carrito):
return sum(PRODUCTOS[int(pid)]["precio"] * cantidad for pid, cantidad in carrito.items())
@app.route("/carrito/agregar/<int:producto_id>", methods=["POST"])
def agregar(producto_id):
if producto_id not in PRODUCTOS:
abort(404)
cantidad = request.form.get("cantidad", 1, type=int)
if cantidad < 1 or cantidad > 10:
abort(400)
carrito = session.get("carrito", {})
clave = str(producto_id)
carrito[clave] = carrito.get(clave, 0) + cantidad
session["carrito"] = carrito
return redirect(url_for("ver_carrito"))PRODUCTOSes un diccionario para el ejemplo; en tu tienda vendrá de la base de datos.- El carrito guarda solo id y cantidad. El precio siempre se busca en el servidor: aunque el formulario envíe
precio=1, se ignora (verificado: total 133 000 con los precios reales). clave = str(producto_id): la sesión se guarda como JSON, y en JSON las claves de un diccionario son texto. Por esototal_carritoconvierte conint(pid).- Se valida la cantidad (entre 1 y 10): una cantidad negativa daría un total negativo (verificado: responde 400).
🪤 Trampa 1: type=int no avisa
request.form.get("cantidad", 1, type=int): si la conversión falla, devuelve el valor por defecto sin error. Verificado: con cantidad="abc" se agregó 1 unidad. ¿Es lo que quieres? Si prefieres rechazar la entrada, no pongas valor por defecto y comprueba si el resultado es None.
🪤 Trampa 2: modificar la sesión "por dentro"
session["carrito"]["1"] = session["carrito"].get("1", 0) + 1 # ¡no se guarda!Verificado: tres peticiones seguidas mostraron 1, 2 y 2. Flask detecta cuando asignas una clave de session, pero no cuando cambias un diccionario dentro de ella. Soluciones: sacar el carrito, modificarlo y reasignarlo (session["carrito"] = carrito, como en el ejemplo), o marcar session.modified = True.
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
| Precio enviado desde el formulario | Cualquiera compra a $1 | El servidor busca el precio |
| Carrito que "olvida" productos | Modificación anidada de session |
Reasignar o session.modified = True |
KeyError al calcular el total |
Un producto del carrito ya no existe | Ignóralo o avísale al usuario |
RuntimeError: The session is unavailable because no secret key was set |
Falta secret_key |
app.secret_key = os.environ["SECRET_KEY"] |
Totales como 133000.00000000001 |
Precios en float |
Enteros en la unidad mínima |
5. 📖 El pago: Stripe Checkout en modo de prueba
(Sección creada para esta adaptación. El código NO se ejecutó; sigue el patrón de la documentación de Stripe Checkout, que debes consultar en su versión actual.)
Cómo funciona una pasarela de pagos
Tu sitio nunca ve el número de tarjeta. Redirige al usuario a una página de la pasarela, que cobra y luego lo devuelve a tu sitio:
Tu servidor ──(1) crear sesión de pago con productos y precios──► Stripe
Tu servidor ◄──(2) URL de la página de pago────────────────────── Stripe
Navegador ───(3) redirección a la página de pago─────────────────► Stripe (el usuario paga)
Navegador ◄──(4) redirección a success_url o cancel_url────────── Stripe
Tu servidor ◄─(5) aviso firmado (webhook): "el pago X se completó"─ StripeModo de prueba
Stripe entrega dos juegos de claves: de prueba (empiezan por sk_test_) y reales (sk_live_). Con las de prueba, se usan tarjetas de prueba de su documentación (por ejemplo, 4242 4242 4242 4242 con cualquier fecha futura) y no se mueve dinero. Todo el proyecto se hace así.
Stripe no está disponible para abrir cuentas en todos los países, y crear una cuenta requiere ser mayor de edad. Para el aula: el docente crea la cuenta de prueba (o se usa otra pasarela con modo sandbox disponible en tu país) y comparte solo las claves de prueba, como variables de entorno.
Esquema de la ruta de pago (no ejecutado)
import os
import stripe
stripe.api_key = os.environ["STRIPE_SECRET_KEY"] # sk_test_...
@app.route("/pagar", methods=["POST"])
@login_required
def pagar():
carrito = session.get("carrito", {})
if not carrito:
return redirect(url_for("ver_carrito"))
lineas = [{
"price_data": {
"currency": "usd",
"product_data": {"name": PRODUCTOS[int(pid)]["nombre"]},
"unit_amount": PRODUCTOS[int(pid)]["precio"], # en la unidad mínima de la moneda
},
"quantity": cantidad,
} for pid, cantidad in carrito.items()]
sesion_pago = stripe.checkout.Session.create(
mode="payment",
line_items=lineas,
success_url=url_for("exito", _external=True) + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url=url_for("ver_carrito", _external=True),
)
return redirect(sesion_pago.url, code=303)Puntos clave:
unit_amountva en la unidad mínima de la moneda (centavos para USD). Consulta en la documentación de Stripe cómo trata la moneda que elijas.- Los precios salen del servidor, igual que en el carrito.
@login_required(Flask-Login) exige sesión iniciada para pagar.- La página de éxito no prueba que se pagó: cualquiera puede escribir esa URL. La confirmación confiable es el webhook firmado de Stripe (paso 5) o consultar la sesión de pago desde el servidor con su
session_id. Solo entonces el pedido pasa a"pagado".
6. 🚀 Proyecto del día: construcción por versiones
(Desarrolla la tarea dia97-tarea16.md, adaptando "pagos reales" a pagos de prueba. Las versiones son una propuesta de esta adaptación.)
Problema
Construir una tienda en línea con catálogo, carrito, pago y cuentas de usuario.
Contexto
El club de robótica vende camisetas y termos para financiar su viaje a una competencia. Quiere una tienda en línea de demostración para mostrarla en la feria.
Objetivo
Una tienda Flask funcional de principio a fin, con pagos en modo de prueba, que un visitante pueda recorrer: registrarse, agregar productos, pagar con una tarjeta de prueba y ver su pedido.
Requisitos funcionales
- Catálogo y detalle de productos desde la base de datos.
- Registro e inicio de sesión (contraseñas con hash).
- Carrito: agregar, cambiar cantidad, quitar, ver total.
- Pago con pasarela en modo de prueba.
- Historial de pedidos del usuario.
Restricciones técnicas
- Solo claves de prueba, en variables de entorno.
- Precios y totales siempre calculados en el servidor.
- El pedido se marca como pagado solo tras una confirmación del servidor (webhook o consulta de la sesión de pago).
- Aviso visible en el sitio: "Tienda de demostración: no se realizan cobros reales".
Planificación
- Comprender (recorre una tienda real). 2. Entradas: productos, usuarios, carrito. 3. Procesos: autenticar, calcular, pagar, confirmar. 4. Salidas: páginas, pedidos. 5. Dividir en módulos. 6. Modelo de datos (sección 3). 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.
Construcción paso a paso
- Versión 1 — Catálogo: modelo
Producto, listado y detalle (reutiliza el blog). - Versión 2 — Usuarios: registro y login (reutiliza el Día 68–69).
- Versión 3 — Carrito: en la sesión, con las pruebas de la sección 4.
- Versión 4 — Pago de prueba: crear la sesión de pago y las páginas de éxito y cancelación.
- Versión 5 — Pedidos: guardar el pedido como
"pendiente", confirmarlo desde el servidor y vaciar el carrito. - Versión final: historial de pedidos, diseño,
README.md(con cómo probar con tarjetas de prueba) y diario.
Pruebas
| # | Caso | Resultado esperado |
|---|---|---|
| 1 | Agregar 2 + 1 camisetas y 1 termo | Total 133 000 |
| 2 | Enviar un precio falso en el formulario |
Se ignora |
| 3 | Cantidad -5 |
400 |
| 4 | Producto inexistente | 404 |
| 5 | Ir a /pagar sin sesión iniciada |
Redirección al login |
| 6 | Visitar la URL de éxito sin pagar | El pedido no se marca como pagado |
| 7 | Pago con tarjeta de prueba de éxito | Pedido "pagado", carrito vacío |
| 8 | Pago con tarjeta de prueba de rechazo | Mensaje de error, pedido no pagado |
| 9 | Revisar el repositorio | No hay claves ni secret_key reales |
🧪 Prueba y depuración
@app.route("/pagar", methods=["POST"])
def pagar():
total = float(request.form["total"])
stripe.api_key = "sk_live_51H..."
...
@app.route("/exito")
def exito():
pedido.estado = "pagado"
db.session.commit()Encuentra cuatro problemas: el total llega del formulario (manipulable) y en float; la clave está en el código y es real (sk_live_); no se exige sesión iniciada; y la página de éxito marca como pagado sin verificar el pago.
⭐ Extensión del proyecto
- 🟢 Reto 1 — Aplicación: control de existencias: no permitir comprar más unidades de las disponibles.
- 🟡 Reto 2 — Ampliación: panel de administración (solo para administradores) para crear y editar productos.
- 🟠 Reto 3 — Desafío: webhook de Stripe con verificación de la firma (sigue su documentación) para confirmar pedidos.
- 🔴 Reto 4 — Pensamiento computacional: dos personas compran la última camiseta al mismo tiempo. Diseña cómo evitar vender una unidad que no existe (¿cuándo descuentas las existencias?, ¿qué pasa si el pago falla?) y escribe los casos de prueba.
7. 🌟 Proyecto extra: tienda de la feria empresarial escolar
(Proyecto creado para esta adaptación; no existe en el curso original.)
Situación
En la feria empresarial del colegio, cada grupo tiene un emprendimiento (postres, artesanías, plantas). Se quiere un sitio de reservas: los visitantes reservan productos en línea y pagan en efectivo al recogerlos en el stand.
Requisitos
- Varios vendedores (grupos), cada uno con sus productos y su panel.
- Reservas con código de recogida y hora; el vendedor marca "entregado".
- Reporte de ventas por grupo al final de la feria.
Decisiones que debes tomar (y justificar)
- ¿Cómo evitas reservas falsas que bloquean productos? (¿límite por persona?, ¿vencimiento?)
- ¿Qué datos del comprador necesitas realmente? (Pide el mínimo: privacidad.)
Casos de prueba
- Un grupo no puede ver ni modificar los productos de otro.
- Una reserva vencida libera las unidades.
8. 🏗️ Mi Portafolio Profesional — Etapa 16
(Proyecto integrador creado para esta adaptación; etapas anteriores en los Días 82 a 96.)
Etapa 16 (hoy):
- Repositorio de la tienda con
.env.example(sin valores reales), pruebas del carrito y de permisos, yREADME.mdcon el diagrama del flujo de pago y las instrucciones de prueba. - Un video corto de un pago de prueba completo.
DIARIO.mddel Día 97 y ficha enproyectos.json.- Revisión de seguridad de la tienda con la tabla de errores frecuentes y la de pruebas.
9. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Por qué el servidor debe calcular los precios y totales?
- ¿Qué garantiza la firma de la sesión de Flask y qué no garantiza?
- ¿Por qué tu sitio nunca ve el número de tarjeta al usar una pasarela?
- ¿Por qué visitar la página de éxito no prueba que se pagó?
Análisis de código
sum(PRODUCTOS[int(pid)]["precio"] * cantidad for pid, cantidad in carrito.items())Con carrito = {"1": 2, "2": 3} y los precios del ejemplo, ¿qué devuelve?
Predicción
print(0.1 + 0.2 == 0.3)¿Qué imprime? ¿Qué tiene que ver con los precios?
Depuración
"Agrego productos al carrito, pero a veces al recargar desaparece el último." ¿Qué trampa de la sección 4 puede ser?
Modificación
Agrega la ruta POST /carrito/quitar/<int:producto_id> que reste una unidad y elimine la clave si llega a 0.
Creación
Escribe formatear_pesos(35000) que devuelva "$35.000" (con punto de miles, como se escribe en Colombia).
10. 🤖 Programar con apoyo de IA
Ciclo de trabajo: formula el problema → inténtalo tú primero → consulta a la IA sobre un punto concreto → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.
Actividad 1 — API de pagos actual. Pide a una IA el código para crear una sesión de Stripe Checkout. Compáralo línea por línea con la documentación oficial vigente: las APIs de pago cambian y las IA pueden mostrar versiones antiguas (por ejemplo, parámetros que ya no se usan).
Actividad 2 — Auditoría. Describe tu flujo de pago (sin claves) y pide a una IA una lista de vulnerabilidades posibles. Convierte cada una en un caso de prueba y comprueba si tu tienda la tiene.
Actividad 3 — Conceptos. Pregunta qué es un webhook y por qué se firma. Explícalo con tus palabras en el diario con un ejemplo de la vida real.
Declara el uso: sección "Uso de IA" en el README.md.
11. 🛠️ Observaciones sobre el material original
| Archivo | Observación | Tratamiento |
|---|---|---|
dia97-tarea16.md |
Pide "cobrar pagos reales" | Para estudiantes de grado décimo, se trabaja solo en modo de prueba (nota inicial y sección 5) |
dia97-tarea16.md |
Sugiere Stripe, que no está disponible en todos los países y exige mayoría de edad | Cuenta gestionada por el docente o pasarela con sandbox disponible localmente |
dia97-tarea16.md |
No menciona la seguridad de precios ni la confirmación del pago | Secciones 2, 4 y 5 |
dia97-tarea16.md |
Enlaza la tienda de un blog como ejemplo | Se conserva como inspiración |
12. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
| Tienda en línea a partir del blog en Flask | dia97-tarea16.md |
Secciones 2 y 3 | eCommerce | Reto 2 | Secciones 6 y 8 |
| Carrito y checkout funcionales | dia97-tarea16.md |
Secciones 4 y 5 | eCommerce | Retos 1 y 4 | Sección 6 |
| Mostrar artículos y cobrar pagos (aquí, de prueba) | dia97-tarea16.md |
Sección 5 | eCommerce | Reto 3 | Sección 6 |
| Autenticación: login y registro | dia97-tarea16.md |
Secciones 2 y 5 (login_required) |
eCommerce | — | Sección 6 |
| Considerar la API de Stripe | dia97-tarea16.md |
Sección 5 | — | Reto 3 | — |
| Momento de reflexión | dia97-tarea16.md |
Sección 6 (versión final) | Pregunta de reflexión | — | Sección 8 |
| (No existe en el curso original) | — | Sesión firmada, precios en el servidor, webhooks | — | Tienda de la feria empresarial | Sección 7 |
13. 📦 Evidencias de aprendizaje del Día 97
- La tienda funcionando: video de un pago de prueba de principio a fin.
- Pruebas del carrito, permisos y confirmación de pago.
- Diagrama del modelo de datos y del flujo de pago.
DIARIO.mddel Día 97 y ficha en el portafolio.- Cuaderno de errores: precios desde el formulario,
floaten dinero, sesión sinsecret_key, modificación anidada desession,type=intsilencioso, página de éxito como prueba de pago, clave real en el código.