Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 97: Proyecto de portafolio — Tienda en línea con carrito, pago y cuentas de usuario

100 Días de Código en Python — Día 97: Proyecto de portafolio — Tienda en línea con carrito, pago y cuentas de usuario

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 97, proyecto de portafolio profesional (categoría Web Development).

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 precio falso; 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" en 1 sin error, y modificar un diccionario dentro de session sin 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

  1. La tarea: tu propia tienda en línea
  2. Analizar el problema: las piezas de una tienda
  3. 📖 Modelo de datos
  4. 📖 El carrito en la sesión (y dos trampas)
  5. 📖 El pago: Stripe Checkout en modo de prueba
  6. 🚀 Proyecto del día: construcción por versiones
  7. 🌟 Proyecto extra: tienda de la feria empresarial escolar
  8. 🏗️ Mi Portafolio Profesional — Etapa 16
  9. 🧩 Comprueba lo aprendido
  10. 🤖 Programar con apoyo de IA
  11. 🛠️ Observaciones sobre el material original
  12. 📋 Matriz de trazabilidad
  13. 📦 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:

Texto
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.)

Texto
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é LineaPedido guarda precio_unitario si el producto ya tiene precio? (Pista: ¿qué pasa con los pedidos antiguos si mañana cambias el precio?)
  • ¿Por qué el estado del 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. Los float acumulan errores de redondeo (prueba 0.1 + 0.2 en 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_key debe ser larga, aleatoria y estar en una variable de entorno.

🔎 Ejemplo explicado

Python
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"))
  • PRODUCTOS es 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 eso total_carrito convierte con int(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"

Python
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:

Texto
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ó"─ Stripe

Modo 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)

Python
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_amount va 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

  1. Catálogo y detalle de productos desde la base de datos.
  2. Registro e inicio de sesión (contraseñas con hash).
  3. Carrito: agregar, cambiar cantidad, quitar, ver total.
  4. Pago con pasarela en modo de prueba.
  5. 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

  1. 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

Python
@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

  1. Varios vendedores (grupos), cada uno con sus productos y su panel.
  2. Reservas con código de recogida y hora; el vendedor marca "entregado".
  3. 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):

  1. Repositorio de la tienda con .env.example (sin valores reales), pruebas del carrito y de permisos, y README.md con el diagrama del flujo de pago y las instrucciones de prueba.
  2. Un video corto de un pago de prueba completo.
  3. DIARIO.md del Día 97 y ficha en proyectos.json.
  4. Revisión de seguridad de la tienda con la tabla de errores frecuentes y la de pruebas.

9. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué el servidor debe calcular los precios y totales?
  2. ¿Qué garantiza la firma de la sesión de Flask y qué no garantiza?
  3. ¿Por qué tu sitio nunca ve el número de tarjeta al usar una pasarela?
  4. ¿Por qué visitar la página de éxito no prueba que se pagó?

Análisis de código

Python
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

Python
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.md del Día 97 y ficha en el portafolio.
  • Cuaderno de errores: precios desde el formulario, float en dinero, sesión sin secret_key, modificación anidada de session, type=int silencioso, página de éxito como prueba de pago, clave real en el código.