Sesión · PyMarket Fase 3: Servicios, Menús y Flujo Completo
// Sesiones 29-32 · Fundamentos de Programación · Profe Diego

PyMarket Fase 3: Servicios, Menús y Flujo Completo

Ya tenemos login y registro. Hoy completamos TODO el sistema: los archivos de datos JSON, los servicios de productos, ventas y administración, y los menús que conectan todo. Al final entenderás el proyecto completo — y estarás listo para construir el tuyo.

🐍 Python 3.x
🛠️ Spyder IDE
🔄 CRUD completo
🛒 Carrito y ventas
🚀 Rumbo a tu proyecto
Progreso de la sesión 0 %
Pasos completados: 0 / 0
0
DA
DANI · Asistente de clase
La clase pasada construimos file_manager.py y auth_service.py. Hoy vamos a completar todos los archivos que faltan. Pero antes, mira el mapa completo del proyecto terminado. Fíjate en las flechas mentales: cada capa solo habla con la capa de abajo. 🗺️
📂 mi_mini_market/
  ├── main.py               ← Punto de entrada (Paso 2)
  ├── 📂 menus/             ← Capa de interfaz
     ├── menu_principal.py  ← Paso 3
     ├── menu_vendedor.py   ← Paso 6
     ├── menu_comprador.py  ← Paso 8
     └── menu_admin.py      ← Paso 9
  ├── 📂 services/          ← Capa de lógica
     ├── auth_service.py    ← ✅ Ya lo hicimos
     ├── usuario_service.py  ← Paso 4
     ├── producto_service.py ← Paso 5
     ├── venta_service.py    ← Paso 7
     └── admin_service.py    ← Paso 9
  ├── 📂 utils/             ← Capa de utilidades
     └── file_manager.py    ← ✅ Ya lo hicimos
  └── 📂 data/              ← Capa de datos
      ├── usuarios.json      ← Paso 1
      ├── productos.json     ← Paso 1
      └── ventas.json        ← Paso 1
👤
Estudiante
¿Y cómo sabe Python que menus y services son "carpetas de código"? ¿No son carpetas normales?
DA
DANI · Asistente de clase
¡Excelente pregunta! Cuando escribes from menus.menu_principal import …, Python busca una carpeta llamada menus a partir de donde ejecutaste el programa (por eso siempre corremos desde main.py). El punto en menus.menu_principal se lee como: carpeta menus → archivo menu_principal.py. Es la dirección postal del código. 📬
🧭
Regla de oro de las capas: las flechas de import siempre apuntan hacia abajo. main.py importa de menus/. Los menús importan de services/. Los servicios importan de utils/. Y solo utils/file_manager.py toca los archivos de data/. Nunca al revés: un servicio jamás importa un menú. Si respetas esto, tu proyecto (CRM, LMS o PQRS) será fácil de arreglar cuando algo falle.
Abrí Spyder y verifiqué que tengo file_manager.py y auth_service.py de la clase pasada funcionando
Identifiqué en el mapa qué archivos faltan por crear
1
DA
DANI · Asistente de clase
En PSEINT los datos vivían en variables y se perdían al cerrar el programa. Aquí no: los guardamos en archivos JSON. Un JSON es solo texto, pero con un formato que Python entiende perfecto: listas con [ ] y diccionarios con { }. ¿Te suenan? ¡Son las mismas estructuras de Python! 📄
👤
Estudiante
¿Tengo que escribir estos archivos a mano?
DA
DANI · Asistente de clase
¡No! El sistema los crea y llena solo cuando registras usuarios o publicas productos (recuerda: guardar_json crea el archivo si no existe). Pero necesitas entender su estructura, porque todo el código que escribiremos hoy lee y modifica estos archivos. Míralos como el "plano" de tus datos. 🏗️
data / usuarios.json — así se ve después de 2 registros
[                                  ← ① El archivo completo es UNA LISTA
    {                              ← ② Cada usuario es UN DICCIONARIO
        "id": 1,                  ← ③ número, SIN comillas
        "nombre": "juan",
        "correo": "juan@gmail.com",
        "password": "1234",       ← texto, CON comillas
        "rol": "vendedor"
    },                             ← ④ coma separa los elementos
    {
        "id": 2,
        "nombre": "maria",
        "correo": "maria@gmail.com",
        "password": "abcd",
        "rol": "comprador"
    }
]
data / productos.json — así se ve con 1 producto publicado
[
    {
        "id": 1,
        "nombre": "Audífonos Bluetooth",
        "categoria": "Tecnología",
        "precio": 85000.0,          ← decimal (float en Python)
        "stock": 10,                 ← entero (int en Python)
        "id_vendedor": 1             ← ⑤ ¡La conexión con usuarios.json!
    }
]
data / ventas.json — así se ve después de 1 compra
[
    {
        "id": 1,
        "id_comprador": 2,           ← apunta al usuario con id 2
        "nombre_comprador": "maria",
        "productos": [               ← ⑥ ¡una lista DENTRO del diccionario!
            {
                "id_producto": 1,
                "nombre": "Audífonos Bluetooth",
                "cantidad": 2,
                "precio_unitario": 85000.0,
                "subtotal": 170000.0
            }
        ],
        "total": 170000.0,
        "fecha": "2026-07-02 10:30:00"
    }
]
1 El archivo es una lista [ ] — Por eso leer_json() devuelve [] cuando el archivo no existe: el resto del código siempre espera una lista, tenga 0 o 100 elementos. Un for sobre una lista vacía simplemente no hace nada — no explota.
2 Cada registro es un diccionario { } — Pares "clave": valor. En Python accedes con corchetes: usuario["nombre"] devuelve "juan". Si escribes mal la clave (usuario["Nombre"] con mayúscula) Python lanza KeyError: las claves distinguen mayúsculas.
3 Tipos de datos en JSON — Texto con comillas dobles ("juan"), números sin comillas (1, 85000.0). Cuando json.load() lee el archivo, convierte cada valor a su tipo Python: str, int, float. Por eso podemos hacer matemáticas con precio sin convertir nada.
4 Las comas importan — Entre elemento y elemento va una coma, pero el último NO lleva. Si editas un JSON a mano y dejas una coma de más, json.load() lanza JSONDecodeError (y gracias a nuestro try/except del file_manager, el sistema devuelve [] en vez de morir… ¡pero pierdes de vista tus datos! Cuidado).
5 "id_vendedor": 1 — la relación entre archivos — El producto NO guarda el nombre del vendedor: guarda su id. Es como una cédula: con el id puedes ir a usuarios.json y encontrar todos sus datos. En bases de datos reales esto se llama llave foránea. Tu proyecto necesitará lo mismo: en un CRM, la interacción guarda el id_cliente; en un LMS, la nota guarda el id_estudiante; en un PQRS, la respuesta guarda el id_peticion.
6 Estructuras anidadas — Una venta contiene una lista de productos dentro del diccionario. Para llegar a la cantidad del primer producto: venta["productos"][0]["cantidad"]. Se lee de izquierda a derecha: entra a la venta → toma la lista productos → toma el elemento 0 → toma su cantidad.
⚠️
Nombres de archivo: siempre en minúsculas. En el código escribimos "data/usuarios.json". Windows no distingue Usuarios.json de usuarios.json, pero Linux y Mac SÍ. Acostúmbrate desde ya: rutas y nombres de archivo en minúsculas, sin tildes y sin espacios. Te ahorrará dolores de cabeza en el demo day.
Entendí que el JSON completo es una lista y cada registro un diccionario
Puedo explicar qué hace id_vendedor en productos.json (relación entre archivos)
Abrí mi data/usuarios.json desde la clase pasada y reconocí la estructura
Escribí en papel cómo accedería a venta["productos"][0]["subtotal"]
2
DA
DANI · Asistente de clase
Te va a sorprender: el archivo más importante del proyecto es el más corto. main.py solo hace una cosa: arrancar el menú principal. Toda la lógica vive en las otras capas. Un buen main.py es como el botón de encendido de un carro: pequeño, pero sin él nada arranca. 🔑
main.py
# ① Importamos la función que dibuja el menú principal
from menus.menu_principal import mostrar_menu_principal

# ② Este if es el "portero" del programa
if __name__ == "__main__":
    mostrar_menu_principal()   # ③ ← estas 4 espacios de indentación son OBLIGATORIOS
1 from menus.menu_principal import mostrar_menu_principal — Se lee: "de la carpeta menus, del archivo menu_principal.py, tráeme la función mostrar_menu_principal". Nota que en el import NO se escribe el .py ni los paréntesis de la función: importas el nombre, no la ejecutas todavía.
2 if __name__ == "__main__": — Python le pone a cada archivo una variable secreta llamada __name__. Si ejecutas el archivo directamente (botón ▶ en Spyder), vale "__main__" y el código de adentro corre. Si otro archivo lo importa, vale otra cosa y NO corre. Así evitas que el programa se ejecute "por accidente" al ser importado. Son dos guiones bajos a cada lado: __name__, no _name_.
3 La indentación ES el idioma de Python — En PSEINT escribías FinSi para cerrar un bloque. Python no tiene "Fin": el bloque se define por los 4 espacios a la izquierda. Todo lo que esté indentado bajo el if pertenece al if. Si quitas la indentación, la línea queda "afuera". Si mezclas espacios y tabulaciones, Python lanza IndentationError. Spyder pone los 4 espacios solo al presionar Tab — déjalo trabajar.
💡
¿Por qué correr siempre desde main.py? Dos razones. Primera: las rutas "data/usuarios.json" son relativas a la carpeta desde donde ejecutas — si corres producto_service.py directo, buscará data/ dentro de services/ y fallará. Segunda: los imports from services... se resuelven desde la raíz del proyecto. En Spyder: abre main.py y presiona F5.
Escribí el import de mostrar_menu_principal en main.py
Escribí el if __name__ == "__main__": con los dobles guiones bajos
Verifiqué que la llamada a la función tiene sus 4 espacios de indentación
3
DA
DANI · Asistente de clase
¿Recuerdas que iniciar_sesion() devuelve el diccionario del usuario o None? Hoy por fin usamos ese valor: el menú principal mira el campo "rol" y decide a cuál de los 3 menús enviarte. Es un enrutador: como el recepcionista de un edificio que te dice a qué piso subir. 🛎️
menus / menu_principal.py
# ① Importamos de las DOS capas que este menú necesita
from services.auth_service import registrar_usuario, iniciar_sesion
from menus.menu_admin import menu_admin
from menus.menu_vendedor import menu_vendedor
from menus.menu_comprador import menu_comprador
from services.producto_service import listar_productos


def mostrar_menu_principal():
    while True:                        # ② ciclo infinito: el menú se repite hasta el break
        print("\n====================================")
        print("        PYMARKET CONSOLE")
        print("====================================")
        print("1. Registrarse")
        print("2. Iniciar sesión")
        print("3. Ver productos disponibles")
        print("4. Salir")

        opcion = input("Seleccione una opción: ")

        if opcion == "1":
            registrar_usuario()

        elif opcion == "2":
            usuario = iniciar_sesion()    # ③ recibimos el dict… o None

            if usuario:                   # ④ None se evalúa como False
                if usuario["rol"] == "admin":
                    menu_admin(usuario)   # ⑤ ¡le PASAMOS el usuario al menú!
                elif usuario["rol"] == "vendedor":
                    menu_vendedor(usuario)
                elif usuario["rol"] == "comprador":
                    menu_comprador(usuario)
            else:
                print("Credenciales incorrectas.")

        elif opcion == "3":
            listar_productos()            # cualquiera puede ver productos sin login

        elif opcion == "4":
            print("Gracias por usar PyMarket Console.")
            break                         # ⑥ única salida del while True

        else:
            print("Opción no válida.")
1 Cinco imports, dos capas — Este archivo importa servicios (la lógica) Y otros menús (para navegar entre pantallas). Fíjate: los menús que importamos aún no existen — los crearemos en los pasos 6, 8 y 9. Por eso hoy el programa no correrá completo hasta el final de la sesión. Así se construye software real: por piezas.
2 while True: — En PSEINT usabas Repetir … Hasta Que. Aquí while True es un ciclo que en principio nunca termina: muestra el menú, procesa la opción, y vuelve a empezar. La única forma de salir es la instrucción break. Sin este ciclo, el programa mostraría el menú una vez y se cerraría.
3 usuario = iniciar_sesion() — Aquí está la magia de las capas: la función vive en auth_service.py, pero su valor de retorno viaja hasta acá y lo guardamos en una variable local. El dato cruzó de la capa de servicios a la capa de interfaz.
4 if usuario: — En Python, None se comporta como False dentro de un if, y un diccionario con datos como True. Es un atajo de if usuario is not None:. Ambas formas son válidas; elige una y sé consistente.
5 menu_admin(usuario) — pasar datos como argumento — Le entregamos el diccionario completo al menú. Así, dentro de menu_vendedor podremos saludar con usuario['nombre'] y filtrar productos con usuario['id']. El usuario logueado "viaja" por todo el sistema como argumento de función.
6 break — Rompe el while inmediatamente. Como es la última pantalla del programa, al salir del ciclo la función termina, main.py termina, y el programa se cierra limpiamente.
⚠️
Mira la escalera de indentación. El if usuario["rol"] == "admin": está DENTRO del if usuario:, que está DENTRO del elif opcion == "2":, que está DENTRO del while True:, que está DENTRO de la función. Cada nivel suma 4 espacios: la línea menu_admin(usuario) lleva 20 espacios. En Python, esa escalera reemplaza los FinSi y FinMientras de PSEINT. Si una línea queda en el nivel equivocado, el programa hace otra cosa sin avisar.
Creé menus/menu_principal.py y escribí los 5 imports
Escribí el while True: con los print() del menú y el input()
Implementé el enrutamiento por rol con if/elif anidados
Agregué el break en la opción 4 y el else de opción inválida
4
DA
DANI · Asistente de clase
En la clase pasada, registrar_usuario() hacía TODO: pedir datos, validar y guardar. Funciona, pero mezcla dos cosas: hablar con el usuario (inputs y prints) y la lógica pura (validar, generar id, guardar). Este archivo separa la lógica pura en funciones pequeñas y reutilizables. 🧩
👤
Estudiante
¿Funciones que llaman a otras funciones del mismo archivo? ¿Eso se puede?
DA
DANI · Asistente de clase
¡Claro! Y es la gracia de este archivo: crear_usuario() llama a buscar_usuario_por_correo() y a generar_id_usuario(), que están unas líneas más arriba. Dentro del mismo archivo no necesitas import: las llamas directo por su nombre. 🪆
services / usuario_service.py — parte 1: los ayudantes
from utils.file_manager import leer_json, guardar_json

RUTA_USUARIOS = "data/usuarios.json"


def obtener_usuarios():
    """
    Retorna la lista completa de usuarios registrados.
    """                                    # ① docstring: documentación oficial de la función
    return leer_json(RUTA_USUARIOS)


def guardar_usuarios(usuarios):
    """
    Guarda la lista completa de usuarios en el archivo JSON.
    """
    guardar_json(RUTA_USUARIOS, usuarios)


def generar_id_usuario():
    """
    Genera un ID automático para un nuevo usuario.
    """
    usuarios = obtener_usuarios()

    if len(usuarios) == 0:
        return 1                        # si no hay nadie, el primero es el 1

    # ② busca el id MÁS ALTO y le suma 1
    ultimo_id = max(usuario["id"] for usuario in usuarios)
    return ultimo_id + 1


def buscar_usuario_por_correo(correo):
    """
    Busca un usuario por correo electrónico.
    Retorna el usuario si existe, de lo contrario retorna None.
    """
    usuarios = obtener_usuarios()

    for usuario in usuarios:
        # ③ .lower() vuelve todo minúsculas antes de comparar
        if usuario["correo"].lower() == correo.lower():
            return usuario

    return None


def crear_usuario(nombre, correo, password, rol):
    """
    Crea un nuevo usuario comprador o vendedor.
    """
    usuarios = obtener_usuarios()

    # ④ reutilizamos la función de arriba — sin import, mismo archivo
    if buscar_usuario_por_correo(correo) is not None:
        return False, "Ya existe un usuario con ese correo."

    nuevo_usuario = {
        "id": generar_id_usuario(),
        "nombre": nombre,
        "correo": correo,
        "password": password,
        "rol": rol
    }

    usuarios.append(nuevo_usuario)
    guardar_usuarios(usuarios)

    # ⑤ ¡devolvemos DOS valores a la vez!
    return True, "Usuario registrado correctamente."
1 Docstrings """ … """ — Un texto entre triples comillas justo debajo del def es la documentación oficial de la función. No es un comentario cualquiera: Spyder lo muestra cuando presionas Ctrl+I sobre el nombre de la función. En la documentación de tu proyecto final, los docstrings cuentan como documentación del código.
2 max(usuario["id"] for usuario in usuarios) — Se lee: "de cada usuario de la lista, toma su id, y de todos esos ids dame el mayor". Es un for comprimido dentro de max(). ¿Por qué es mejor que len(usuarios) + 1 que usamos la clase pasada? Porque si eliminas al usuario 2 de 3, len()+1 daría 3… ¡un id que ya existe! Con max()+1 nunca se repite.
3 .lower() — Método de los strings que devuelve el texto en minúsculas. Al aplicarlo a AMBOS lados de la comparación, "Juan@Gmail.com" y "juan@gmail.com" se consideran el mismo correo. Detalle pequeño, experiencia de usuario grande.
4 Funciones que usan funcionescrear_usuario no repite el código de búsqueda: llama a buscar_usuario_por_correo(). Cada función hace UNA cosa y las demás la reutilizan. Cuando en tu proyecto notes que copias y pegas un bloque, detente: eso debe ser una función.
5 return True, "mensaje" — retorno múltiple — Python permite devolver varios valores separados por coma (técnicamente, una tupla). Quien la llama los recibe así: estado, mensaje = crear_usuario(...). El primer valor dice SI funcionó, el segundo QUÉ pasó. Nota clave: esta función no tiene ni un print ni un input — es lógica pura, y quien imprime el mensaje es el menú. Esa separación es la que te pedirán en tu proyecto.
📦
El archivo continúa con listar_usuarios(), actualizar_usuario(id_usuario) y eliminar_usuario(id_usuario) — el UPDATE y DELETE del CRUD de usuarios. Los verás en acción con productos en el Paso 5 (el patrón es idéntico: buscar por id → si no existe avisar → modificar o eliminar → guardar). Escríbelos como reto en casa siguiendo ese patrón; el código de productos del siguiente paso es tu guía.
Creé services/usuario_service.py con el import y la constante de ruta
Escribí obtener_usuarios() y guardar_usuarios() con sus docstrings
Escribí generar_id_usuario() con max(...) y entiendo por qué es mejor que len()+1
Escribí buscar_usuario_por_correo() con .lower()
Escribí crear_usuario() y entiendo el return True, mensaje
5
DA
DANI · Asistente de clase
Este es el archivo más grande del proyecto, pero no te asustes: son las mismas 4 operaciones CRUD repetidas con pequeñas variaciones. Si entiendes este archivo, entiendes el 80% de tu proyecto final, porque un CRM, un LMS y un PQRS son… CRUDs con otros nombres. 💪
services / producto_service.py — parte 1: listar y crear
from utils.file_manager import leer_json, guardar_json

RUTA_PRODUCTOS = "data/productos.json"


def listar_productos():
    productos = leer_json(RUTA_PRODUCTOS)

    print("\n===== PRODUCTOS DISPONIBLES =====")

    if len(productos) == 0:
        print("No hay productos publicados.")
        return                            # ① salida temprana: no hay nada que mostrar

    for producto in productos:
        if producto["stock"] > 0:       # ② solo mostramos lo que hay en bodega
            print("--------------------------------")
            print(f"ID: {producto['id']}")
            print(f"Nombre: {producto['nombre']}")
            print(f"Categoría: {producto['categoria']}")
            print(f"Precio: ${producto['precio']}")
            print(f"Stock: {producto['stock']}")


def crear_producto(usuario):              # ③ recibe al vendedor logueado como parámetro
    productos = leer_json(RUTA_PRODUCTOS)

    print("\n===== PUBLICAR PRODUCTO =====")

    nombre = input("Nombre del producto: ")
    categoria = input("Categoría: ")

    try:                                  # ④ input() da texto; lo convertimos a número
        precio = float(input("Precio: "))
        stock = int(input("Stock: "))
    except ValueError:
        print("Precio o stock inválido.")
        return

    if precio <= 0 or stock < 0:        # ⑤ regla de negocio, no de formato
        print("El precio debe ser mayor a 0 y el stock no puede ser negativo.")
        return

    nuevo_producto = {
        "id": generar_id_producto(productos),
        "nombre": nombre,
        "categoria": categoria,
        "precio": precio,
        "stock": stock,
        "id_vendedor": usuario["id"]    # ⑥ ¡aquí nace la relación entre archivos!
    }

    productos.append(nuevo_producto)
    guardar_json(RUTA_PRODUCTOS, productos)

    print("Producto publicado correctamente.")


def generar_id_producto(productos):
    if len(productos) == 0:
        return 1

    ultimo_id = max(producto["id"] for producto in productos)
    return ultimo_id + 1
1 Salida temprana (early return) — Si no hay productos, avisamos y salimos con return. Patrón que verás en TODO el proyecto: primero se descartan los casos donde no hay nada que hacer, y el "camino feliz" queda al final sin anidar tantos if.
2 if producto["stock"] > 0: — Un if DENTRO del for: se evalúa producto por producto. Los agotados existen en el archivo pero no se muestran al comprador. También ve las f-strings: f"ID: {producto['id']}" incrusta el valor dentro del texto. Ojo al detalle: afuera comillas dobles, adentro comillas simples para la clave — así no chocan.
3 def crear_producto(usuario): — ¿Recuerdas que menu_principal le pasó el usuario a menu_vendedor(usuario)? El menú se lo pasa ahora a esta función. El diccionario del vendedor viajó: auth_service → menu_principal → menu_vendedor → crear_producto. Así sabemos QUIÉN publica sin volver a preguntar.
4 float(), int() y try/except ValueErrorinput() SIEMPRE devuelve texto: "85000" no es lo mismo que 85000. float() convierte a decimal e int() a entero. Pero si el usuario escribe "abc", la conversión lanza un error llamado ValueError. El try dice "intenta esto", y el except "si falla, haz esto otro en vez de morir". Sin él, el programa se cerraría de golpe frente a tu profe en el demo day. 😅
5 Validar formato ≠ validar negocio — El try/except valida que sea un número (formato). Este if valida que el número tenga sentido (negocio): un precio de -500 es un número válido pero un dato absurdo. Tu proyecto necesita ambos tipos de validación.
6 "id_vendedor": usuario["id"] — El momento exacto donde se crea la relación que vimos en el Paso 1: guardamos el id del vendedor dentro del producto. Gracias a esto, más adelante podremos responder "¿cuáles son MIS productos?" y "¿cuánto ha vendido ESTE vendedor?".
services / producto_service.py — parte 2: mis productos y buscar
def listar_mis_productos(usuario):
    productos = leer_json(RUTA_PRODUCTOS)

    # ⑦ list comprehension: filtra la lista en 4 líneas
    mis_productos = [
        producto for producto in productos
        if producto["id_vendedor"] == usuario["id"]
    ]

    print("\n===== MIS PRODUCTOS =====")

    if len(mis_productos) == 0:
        print("No tienes productos publicados.")
        return

    for producto in mis_productos:
        print("--------------------------------")
        print(f"ID: {producto['id']}")
        print(f"Nombre: {producto['nombre']}")
        print(f"Categoría: {producto['categoria']}")
        print(f"Precio: ${producto['precio']}")
        print(f"Stock: {producto['stock']}")


def buscar_producto_por_nombre():
    productos = leer_json(RUTA_PRODUCTOS)

    print("\n===== BUSCAR PRODUCTO =====")

    if len(productos) == 0:
        print("No hay productos registrados.")
        return

    nombre_busqueda = input("Ingrese el nombre del producto: ").lower()

    encontrados = []                      # lista vacía que iremos llenando

    for producto in productos:
        # ⑧ "in" pregunta si un texto está CONTENIDO en otro
        if nombre_busqueda in producto["nombre"].lower():
            encontrados.append(producto)

    if len(encontrados) == 0:
        print("No se encontraron productos.")
        return

    print("\n===== RESULTADOS =====")

    for producto in encontrados:
        print("--------------------------------")
        print(f"ID: {producto['id']}")
        print(f"Nombre: {producto['nombre']}")
        print(f"Categoría: {producto['categoria']}")
        print(f"Precio: ${producto['precio']}")
        print(f"Stock: {producto['stock']}")
7 List comprehension[producto for producto in productos if producto["id_vendedor"] == usuario["id"]] se lee: "dame una lista nueva con cada producto de la lista original, PERO solo los que cumplan la condición". Es el equivalente compacto de crear una lista vacía + for + if + append. Compárala con buscar_producto_por_nombre justo debajo, que hace lo mismo en la versión larga: ambas son válidas, la comprehension es la forma "pythonica".
8 in para buscar dentro de texto"audi" in "audífonos bluetooth" es True. Por eso el usuario no necesita escribir el nombre completo: con un pedazo basta. Combinado con .lower() en ambos lados, la búsqueda ignora mayúsculas. Este mismo in también sirve para listas: 3 in [1, 2, 3] es True — lo usaremos en ventas.
services / producto_service.py — parte 3: actualizar y eliminar
def actualizar_producto(usuario):
    productos = leer_json(RUTA_PRODUCTOS)

    mis_productos = [
        producto for producto in productos
        if producto["id_vendedor"] == usuario["id"]
    ]

    print("\n===== ACTUALIZAR PRODUCTO =====")

    if len(mis_productos) == 0:
        print("No tienes productos para actualizar.")
        return

    listar_mis_productos(usuario)         # reutilizamos: mostrar antes de elegir

    try:
        id_producto = int(input("\nIngrese el ID del producto a actualizar: "))
    except ValueError:
        print("Debe ingresar un ID válido.")
        return

    # ⑨ patrón "buscar y quedarse con la referencia"
    producto_encontrado = None

    for producto in productos:
        if producto["id"] == id_producto and producto["id_vendedor"] == usuario["id"]:
            producto_encontrado = producto
            break                         # ya lo encontramos: no hay que seguir buscando

    if producto_encontrado is None:
        print("No se encontró un producto suyo con ese ID.")
        return

    print("\nDeje el campo vacío si no desea modificarlo.")

    # ⑩ mostramos el valor actual entre paréntesis como pista
    nuevo_nombre = input(f"Nuevo nombre ({producto_encontrado['nombre']}): ")
    nueva_categoria = input(f"Nueva categoría ({producto_encontrado['categoria']}): ")
    nuevo_precio = input(f"Nuevo precio ({producto_encontrado['precio']}): ")
    nuevo_stock = input(f"Nuevo stock ({producto_encontrado['stock']}): ")

    if nuevo_nombre != "":                # ⑪ vacío = "no quiero cambiarlo"
        producto_encontrado["nombre"] = nuevo_nombre

    if nueva_categoria != "":
        producto_encontrado["categoria"] = nueva_categoria

    if nuevo_precio != "":
        try:
            nuevo_precio = float(nuevo_precio)
            if nuevo_precio <= 0:
                print("El precio debe ser mayor a 0.")
                return
            producto_encontrado["precio"] = nuevo_precio
        except ValueError:
            print("Precio inválido.")
            return

    if nuevo_stock != "":
        try:
            nuevo_stock = int(nuevo_stock)
            if nuevo_stock < 0:
                print("El stock no puede ser negativo.")
                return
            producto_encontrado["stock"] = nuevo_stock
        except ValueError:
            print("Stock inválido.")
            return

    guardar_json(RUTA_PRODUCTOS, productos)  # ⑫ guardamos la lista COMPLETA

    print("Producto actualizado correctamente.")


def eliminar_producto(usuario):
    productos = leer_json(RUTA_PRODUCTOS)

    mis_productos = [
        producto for producto in productos
        if producto["id_vendedor"] == usuario["id"]
    ]

    print("\n===== ELIMINAR PRODUCTO =====")

    if len(mis_productos) == 0:
        print("No tienes productos para eliminar.")
        return

    listar_mis_productos(usuario)

    try:
        id_producto = int(input("\nIngrese el ID del producto a eliminar: "))
    except ValueError:
        print("Debe ingresar un ID válido.")
        return

    producto_encontrado = None

    for producto in productos:
        if producto["id"] == id_producto and producto["id_vendedor"] == usuario["id"]:
            producto_encontrado = producto
            break

    if producto_encontrado is None:
        print("No se encontró un producto suyo con ese ID.")
        return

    # ⑬ pedir confirmación antes de una acción destructiva
    confirmacion = input(
        f"¿Está seguro de eliminar el producto '{producto_encontrado['nombre']}'? (s/n): "
    ).lower()

    if confirmacion == "s":
        productos.remove(producto_encontrado)   # ⑭ saca el elemento de la lista
        guardar_json(RUTA_PRODUCTOS, productos)
        print("Producto eliminado correctamente.")
    else:
        print("Eliminación cancelada.")
9 El patrón "buscar y quedarse con la referencia" — Arrancamos con producto_encontrado = None (aún no hay nada), recorremos, y al encontrar coincidencia la guardamos y cortamos con break. Si el for termina y la variable sigue en None, no existía. Nota la doble condición con and: el id debe coincidir Y el producto debe ser TUYO — un vendedor no puede editar productos ajenos. Eso es seguridad por rol, y tu proyecto la necesita.
10 UX en consola — Mostrar el valor actual entre paréntesis (Nuevo precio (85000.0):) le recuerda al usuario qué hay guardado. Detalles así suman puntos en el demo day.
11 Campo vacío = no modificar — Si el usuario solo presiona Enter, input() devuelve "" (cadena vacía) y el if se salta la asignación: el valor viejo sobrevive. Así puedes cambiar SOLO el precio sin re-escribir todo.
12 ¿Por qué guardamos productos y no producto_encontrado? — Truco importante de Python: producto_encontrado NO es una copia, es una referencia al mismo diccionario que vive dentro de la lista productos. Al modificarlo, la lista ya quedó modificada en memoria. Solo falta escribir la lista completa al disco.
13 Confirmación (s/n) — Eliminar no tiene deshacer. Siempre pregunta antes. El .lower() al final del input acepta tanto "S" como "s".
14 lista.remove(elemento) — Busca el elemento dentro de la lista y lo saca. Compara: append() agrega al final, remove() quita. De nuevo, después de modificar en memoria hay que persistir con guardar_json().
🔁
¿Notaste el patrón? actualizar y eliminar comparten el mismo esqueleto: leer → filtrar los míos → mostrar → pedir id → buscar → validar que existe → actuar → guardar. Memorízalo: es la receta de cualquier UPDATE/DELETE de tu proyecto. Cambia "producto" por "cliente" (CRM), "curso" (LMS) o "petición" (PQRS) y el código es casi el mismo.
Creé services/producto_service.py con import y constante RUTA_PRODUCTOS
Escribí listar_productos() con el filtro de stock > 0
Escribí crear_producto(usuario) con try/except ValueError y validaciones
Escribí generar_id_producto(productos) con max()
Escribí listar_mis_productos(usuario) y entiendo la list comprehension
Escribí actualizar_producto(usuario) con el patrón buscar → validar → modificar → guardar
Escribí eliminar_producto(usuario) con confirmación (s/n) y remove()
Escribí buscar_producto_por_nombre() con in y .lower()
6
DA
DANI · Asistente de clase
Con el servicio listo, el menú es pan comido: un while True que muestra opciones y delega TODO al servicio. Fíjate que el menú no tiene lógica de negocio: no valida precios, no toca JSON. Solo dibuja y enruta. 🎨
menus / menu_vendedor.py
# ① import multilínea: paréntesis para no hacer una línea kilométrica
from services.producto_service import (
    crear_producto,
    listar_mis_productos,
    actualizar_producto,
    eliminar_producto
)


def menu_vendedor(usuario):              # ② recibe el dict que envió menu_principal
    while True:
        print("\n========== MENÚ VENDEDOR ==========")
        print(f"Bienvenido vendedor: {usuario['nombre']}")
        print("1. Publicar producto")
        print("2. Ver mis productos")
        print("3. Actualizar producto")
        print("4. Eliminar producto")
        print("5. Cerrar sesión")

        opcion = input("Seleccione una opción: ")

        if opcion == "1":
            crear_producto(usuario)       # ③ el usuario sigue viajando hacia abajo

        elif opcion == "2":
            listar_mis_productos(usuario)

        elif opcion == "3":
            actualizar_producto(usuario)

        elif opcion == "4":
            eliminar_producto(usuario)

        elif opcion == "5":
            print("Sesión cerrada correctamente.")
            break                         # ④ vuelve al menú principal, NO cierra el programa

        else:
            print("Opción no válida. Intente nuevamente.")
1 Import con paréntesis — Cuando importas muchas funciones, los paréntesis permiten partir el import en varias líneas sin usar \. Cada función va en su renglón con coma. Más legible y más fácil de modificar.
2 menu_vendedor(usuario) — El parámetro llega desde menu_principal, que lo recibió de iniciar_sesion(). Por eso podemos saludar por nombre sin volver a preguntar quién eres.
3 El menú delega, el servicio trabaja — Cada opción es una sola línea: llamar al servicio pasándole el usuario. Si mañana cambias cómo se crea un producto, este archivo NO se toca. Esa es la recompensa de las capas.
4 break ≠ cerrar el programa — Este break solo rompe el while de ESTE menú. La función termina y el control regresa a quien la llamó: menu_principal, cuyo propio while True sigue vivo y vuelve a mostrar sus opciones. Los menús se apilan como cajas: al cerrar la de adentro, sigues dentro de la de afuera.
Creé menus/menu_vendedor.py con el import multilínea entre paréntesis
Escribí el while True con las 5 opciones y el saludo con f-string
Probé: registré un vendedor, inicié sesión y publiqué un producto de prueba
Abrí data/productos.json y vi mi producto guardado con su id_vendedor
7
DA
DANI · Asistente de clase
Este es el archivo más interesante del proyecto porque el carrito NO se guarda en JSON: vive en la memoria RAM mientras dura la sesión, como en cualquier tienda online. Solo cuando confirmas la compra, todo se vuelve permanente: se crea la venta Y se descuenta el stock. Dos archivos modificados en una sola operación. 🛒
👤
Estudiante
¿Y dónde "vive" el carrito si no está en un archivo?
DA
DANI · Asistente de clase
Es una lista de Python que se crea en menu_comprador (lo verás en el Paso 8) y se pasa como argumento a estas funciones. Cuando cierras sesión, la lista muere y el carrito se vacía. Por eso, si sales sin comprar, pierdes el carrito — igual que en la vida real cuando abandonas el carrito en el supermercado. 😄
services / venta_service.py — parte 1: el carrito
from utils.file_manager import leer_json, guardar_json
from datetime import datetime          # ① librería estándar para fechas y horas

RUTA_PRODUCTOS = "data/productos.json"
RUTA_VENTAS = "data/ventas.json"       # este servicio usa DOS archivos


def agregar_al_carrito(carrito):        # ② recibe la lista que vive en el menú
    productos = leer_json(RUTA_PRODUCTOS)

    if len(productos) == 0:
        print("No hay productos disponibles.")
        return

    print("\n===== AGREGAR PRODUCTO AL CARRITO =====")

    try:
        id_producto = int(input("Ingrese el ID del producto: "))
        cantidad = int(input("Ingrese la cantidad: "))
    except ValueError:
        print("Debe ingresar valores numéricos.")
        return

    producto_encontrado = None            # el patrón de búsqueda que ya conoces

    for producto in productos:
        if producto["id"] == id_producto:
            producto_encontrado = producto
            break

    if producto_encontrado is None:
        print("Producto no encontrado.")
        return

    if cantidad <= 0:
        print("La cantidad debe ser mayor a cero.")
        return

    if cantidad > producto_encontrado["stock"]:   # ③ no vender lo que no hay
        print("No hay suficiente stock disponible.")
        return

    # ④ el item del carrito es un diccionario NUEVO, hecho a la medida
    item_carrito = {
        "id_producto": producto_encontrado["id"],
        "nombre": producto_encontrado["nombre"],
        "cantidad": cantidad,
        "precio_unitario": producto_encontrado["precio"],
        "subtotal": cantidad * producto_encontrado["precio"]
    }

    carrito.append(item_carrito)         # ⑤ modificamos la lista del menú ¡sin return!

    print("Producto agregado al carrito correctamente.")


def ver_carrito(carrito):
    print("\n===== CARRITO DE COMPRAS =====")

    if len(carrito) == 0:
        print("El carrito está vacío.")
        return

    total = 0                             # ⑥ acumulador: arranca en 0 y crece

    for item in carrito:
        print("--------------------------------")
        print(f"Producto: {item['nombre']}")
        print(f"Cantidad: {item['cantidad']}")
        print(f"Precio unitario: ${item['precio_unitario']}")
        print(f"Subtotal: ${item['subtotal']}")

        total += item["subtotal"]        # total = total + subtotal

    print("--------------------------------")
    print(f"TOTAL A PAGAR: ${total}")
1 from datetime import datetime — Librería estándar (no se instala). Sí, el nombre se repite: el módulo datetime contiene una clase llamada datetime. La usaremos para sellar cada venta con fecha y hora exactas.
2 agregar_al_carrito(carrito) — La lista carrito NO se crea aquí: nace en menu_comprador y llega como argumento. En Python, las listas se pasan por referencia: la función recibe la lista original, no una copia. Todo lo que le haga aquí, el menú lo verá.
3 Validar contra el stock — Tres validaciones en cascada, cada una con salida temprana: ¿existe el producto? ¿la cantidad es positiva? ¿hay stock suficiente? El orden importa: no tiene sentido validar stock de un producto que no existe.
4 El item guarda una "foto" del precio — Copiamos precio_unitario al carrito en vez de solo el id. ¿Por qué? Si el vendedor sube el precio 5 minutos después, tu carrito conserva el precio que viste. Y calculamos subtotal de una vez: cantidad * precio.
5 Modificar sin return — Esta función no devuelve nada: su efecto es agregar el item a la lista compartida. Como el menú tiene la MISMA lista (referencia, no copia), el cambio persiste entre llamadas. Así el carrito "recuerda" entre una opción del menú y la siguiente.
6 El acumulador — El patrón clásico que viste en PSEINT: total = 0 antes del ciclo, total += item["subtotal"] dentro. += es el atajo de total = total + …. Al salir del for, total tiene la suma completa.
services / venta_service.py — parte 2: realizar la compra
def realizar_compra(usuario, carrito):  # recibe DOS argumentos
    if len(carrito) == 0:
        print("No puede realizar una compra con el carrito vacío.")
        return

    productos = leer_json(RUTA_PRODUCTOS)
    ventas = leer_json(RUTA_VENTAS)

    # ⑦ Validación por si ventas.json está como {} o vacío mal estructurado
    if not isinstance(ventas, list):
        ventas = []

    total = 0

    for item in carrito:
        total += item["subtotal"]

    nueva_venta = {
        "id": len(ventas) + 1,
        "id_comprador": usuario["id"],
        "nombre_comprador": usuario["nombre"],
        "productos": carrito.copy(),      # ⑧ una COPIA, no la lista original
        "total": total,
        "fecha": datetime.now().strftime("%Y-%m-%d %H:%M:%S")  # ⑨
    }

    # ⑩ for anidado: por cada item del carrito, buscar su producto y descontar
    for item in carrito:
        for producto in productos:
            if producto["id"] == item["id_producto"]:
                producto["stock"] -= item["cantidad"]

    ventas.append(nueva_venta)

    guardar_json(RUTA_VENTAS, ventas)     # ⑪ persistimos LOS DOS archivos
    guardar_json(RUTA_PRODUCTOS, productos)

    carrito.clear()                       # ⑫ vaciar el carrito tras comprar

    print("\nCompra realizada correctamente.")
    print(f"Total pagado: ${total}")


def ver_historial_compras(usuario):
    ventas = leer_json(RUTA_VENTAS)

    compras_usuario = [                   # filtrar MIS compras: comprehension otra vez
        venta for venta in ventas
        if venta["id_comprador"] == usuario["id"]
    ]

    print("\n===== HISTORIAL DE COMPRAS =====")

    if len(compras_usuario) == 0:
        print("No tienes compras registradas.")
        return

    for venta in compras_usuario:
        print("--------------------------------")
        print(f"ID venta: {venta['id']}")
        print(f"Fecha: {venta['fecha']}")
        print(f"Total: ${venta['total']}")
        print("Productos:")

        for producto in venta["productos"]:   # ⑬ for dentro del for: lista anidada
            print(
                f"- {producto['nombre']} | "
                f"Cantidad: {producto['cantidad']} | "
                f"Subtotal: ${producto['subtotal']}"
            )
7 isinstance(ventas, list) — Pregunta si la variable es del tipo lista. Programación defensiva: si alguien dejó ventas.json con {} en vez de [], el append de abajo fallaría. Mejor detectarlo y corregir a [].
8 carrito.copy() — el detalle más fino del proyecto — Si guardáramos carrito directo, la venta apuntaría a LA MISMA lista… que dos líneas después vaciamos con clear(). ¡La venta quedaría sin productos! copy() crea una lista nueva independiente. Recuerda: asignar una lista NO la copia, solo crea otro nombre para la misma caja.
9 datetime.now().strftime("%Y-%m-%d %H:%M:%S")now() da la fecha/hora actual; strftime ("string format time") la convierte a texto con el formato que pidas: %Y año de 4 cifras, %m mes, %d día, %H:%M:%S hora:minuto:segundo. Resultado: "2026-07-02 10:30:00". Un JSON no puede guardar objetos fecha, solo texto — por eso la conversión.
10 El for anidado que descuenta stock — Por cada item comprado (for externo) recorremos todos los productos (for interno) hasta hallar el suyo, y restamos: producto["stock"] -= item["cantidad"]. Como producto es una referencia al diccionario dentro de productos, la lista completa queda actualizada en memoria.
11 Guardar DOS archivos — La compra cambia dos realidades: nace una venta Y baja el stock. Si guardáramos solo uno, los datos quedarían inconsistentes (venta sin descuento de inventario). En bases de datos reales esto se llama transacción.
12 carrito.clear() — Vacía la lista original (la del menú, por referencia). El comprador puede iniciar otra compra con carrito limpio, sin cerrar sesión.
13 Recorrer la lista anidada — Cada venta trae su lista "productos" adentro (¿recuerdas el Paso 1?). Un for dentro de otro for: el externo recorre ventas, el interno los productos de ESA venta. Y mira el print de varias líneas: varias f-strings seguidas entre paréntesis se unen solas en un solo texto.
📦
El archivo cierra con ver_ventas_vendedor(usuario): filtra los productos del vendedor, junta sus ids en una lista, y recorre las ventas preguntando con if item["id_producto"] in ids_productos_vendedor: cuáles items le pertenecen, acumulando total_vendido. Usa solo herramientas que ya conoces: comprehensions, in sobre listas y el acumulador. Escríbela como reto guiándote del código completo del repositorio del curso.
Creé services/venta_service.py con los imports (incluido datetime) y las DOS rutas
Escribí agregar_al_carrito(carrito) con sus 3 validaciones en cascada
Escribí ver_carrito(carrito) con el acumulador de total
Escribí realizar_compra(usuario, carrito) y entiendo por qué carrito.copy()
Entiendo por qué se guardan DOS archivos JSON en la compra
Escribí ver_historial_compras(usuario) con el for anidado
8
DA
DANI · Asistente de clase
Mira la primera línea de la función: carrito = []. Ese es el momento exacto en que nace el carrito: una lista vacía creada al entrar al menú, ANTES del while. Todas las opciones comparten esa misma lista. 🛍️
menus / menu_comprador.py
from services.producto_service import listar_productos, buscar_producto_por_nombre
from services.venta_service import agregar_al_carrito, ver_carrito, realizar_compra, ver_historial_compras


def menu_comprador(usuario):
    carrito = []                          # ① nace ANTES del while → sobrevive cada vuelta

    while True:
        print("\n========== MENÚ COMPRADOR ==========")
        print("1. Ver productos disponibles")
        print("2. Buscar producto por nombre")
        print("3. Agregar producto al carrito")
        print("4. Ver carrito")
        print("5. Realizar compra")
        print("6. Ver historial de compras")
        print("7. Cerrar sesión")

        opcion = input("Seleccione una opción: ")

        if opcion == "1":
            listar_productos()

        elif opcion == "2":
            buscar_producto_por_nombre()

        elif opcion == "3":
            agregar_al_carrito(carrito)   # ② la MISMA lista viaja a cada función

        elif opcion == "4":
            ver_carrito(carrito)

        elif opcion == "5":
            realizar_compra(usuario, carrito)  # ③ dos argumentos: quién y qué

        elif opcion == "6":
            ver_historial_compras(usuario)

        elif opcion == "7":
            print("Sesión cerrada correctamente.")
            break                         # ④ al salir, el carrito muere con la función

        else:
            print("Opción no válida. Intente nuevamente.")
1 carrito = [] antes del while — La posición lo es todo. Si estuviera DENTRO del while, cada vuelta del menú crearía un carrito nuevo vacío y perderías lo agregado. Al estar antes, la lista sobrevive todas las vueltas mientras no salgas de la función. Es una variable local de menu_comprador: nadie más la ve, solo las funciones a las que se la pasamos.
2 La misma lista para todos — Opción 3 le agrega items, opción 4 la muestra, opción 5 la usa y la vacía. Todas reciben la MISMA lista por referencia. Por eso el carrito "recuerda" lo que agregaste aunque pases por varias opciones del menú.
3 realizar_compra(usuario, carrito) — El orden de los argumentos importa y debe coincidir con el def realizar_compra(usuario, carrito): del servicio. Primero quién compra, luego qué compra.
4 El ciclo de vida del carrito — Nace al entrar al menú (login), vive mientras navegas, y muere con el break (logout). Si vuelves a iniciar sesión, arranca vacío otra vez. Los datos permanentes van a JSON; los temporales, a variables. Decidir cuál es cuál será una de las preguntas clave de TU proyecto.
Creé menus/menu_comprador.py con los imports de los 2 servicios
Escribí carrito = [] ANTES del while True y sé explicar por qué
Conecté las 7 opciones con sus servicios pasando carrito y usuario
Probé el flujo completo: agregar al carrito → ver carrito → comprar → ver historial
Abrí data/productos.json y confirmé que el stock bajó tras la compra
9
DA
DANI · Asistente de clase
Buenas noticias: aquí ya no hay sintaxis nueva. El servicio del admin usa las mismas herramientas de siempre, con una diferencia de permisos: el admin ve TODOS los usuarios, TODOS los productos y TODAS las ventas, sin filtrar por id. Comparar su código con el del vendedor te muestra cómo se programan los roles. 🕶️
👤
Estudiante
Pero en el registro solo se puede elegir comprador o vendedor… ¿de dónde sale el admin?
DA
DANI · Asistente de clase
¡Muy bien visto! El admin no se registra por el menú: se crea editando usuarios.json a mano y poniendo "rol": "admin". Es una decisión de diseño: nadie puede auto-nombrarse administrador desde la aplicación. En tu proyecto harán lo mismo con el rol de coordinador/administrador. 🔐
services / admin_service.py — fragmento: lo distinto del admin
from utils.file_manager import leer_json, guardar_json

RUTA_USUARIOS = "data/usuarios.json"   # ① el admin usa LOS TRES archivos
RUTA_PRODUCTOS = "data/productos.json"
RUTA_VENTAS = "data/ventas.json"


def ver_usuarios():
    usuarios = leer_json(RUTA_USUARIOS)

    print("\n===== USUARIOS REGISTRADOS =====")

    if len(usuarios) == 0:
        print("No hay usuarios registrados.")
        return

    for usuario in usuarios:              # ② SIN filtro: los ve todos
        print("--------------------------------")
        print(f"ID: {usuario['id']}")
        print(f"Nombre: {usuario['nombre']}")
        print(f"Correo: {usuario['correo']}")
        print(f"Rol: {usuario['rol']}")      # nota: NUNCA imprimimos el password


def ver_ventas_generales():
    ventas = leer_json(RUTA_VENTAS)

    print("\n===== VENTAS GENERALES =====")

    if len(ventas) == 0:
        print("No hay ventas registradas.")
        return

    total_general = 0                     # ③ acumulador sobre TODAS las ventas

    for venta in ventas:
        print("--------------------------------")
        print(f"ID venta: {venta['id']}")
        print(f"Comprador: {venta['nombre_comprador']}")
        print(f"Fecha: {venta['fecha']}")
        print(f"Total venta: ${venta['total']}")

        for producto in venta["productos"]:
            print(f"- {producto['nombre']} | Cantidad: {producto['cantidad']}")

        total_general += venta["total"]

    print("--------------------------------")
    print(f"TOTAL GENERAL DE VENTAS: ${total_general}")
1 Tres rutas, un servicio — El admin supervisa todo el sistema, así que su servicio declara las tres constantes. Compáralo con producto_service (una ruta) y venta_service (dos): cada servicio conoce SOLO los archivos que necesita.
2 El rol se nota en el filtro (o su ausencia)listar_mis_productos filtraba con if producto["id_vendedor"] == usuario["id"]. El admin recorre sin if: lo ve todo. La diferencia entre roles no es magia, son filtros. Detalle de seguridad: mostramos el rol pero JAMÁS el password por pantalla.
3 Reporte con acumulador — La misma técnica de ver_carrito, pero sumando el total de TODAS las ventas del sistema. Los "reportes" que pide tu proyecto final son exactamente esto: recorrer, filtrar y acumular.
📦
Completa el archivo con ver_productos() (igual que ver_usuarios pero con productos, mostrando también id_vendedor) y eliminar_producto_admin() (el mismo patrón eliminar del Paso 5, pero SIN la condición de dueño: el admin puede eliminar cualquier producto). Luego crea menus/menu_admin.py: es un calco de menu_vendedor.py con 5 opciones que llaman a ver_usuarios(), ver_productos(), eliminar_producto_admin(), ver_ventas_generales() y el break para cerrar sesión. ¡Ya tienes todas las piezas para escribirlo solo!
Creé services/admin_service.py con las 3 rutas y ver_usuarios()
Escribí ver_productos(), eliminar_producto_admin() y ver_ventas_generales()
Creé menus/menu_admin.py siguiendo el patrón de menu_vendedor.py
Edité usuarios.json a mano para crear mi usuario con "rol": "admin"
Corrí main.py (F5) y probé los 3 roles de punta a punta 🎉
10
DA
DANI · Asistente de clase
Y aquí viene la revelación final: PyMarket ES tu proyecto, solo que con otros nombres. Los tres proyectos tienen usuarios con roles, entidades que se crean/listan/actualizan/eliminan, relaciones por id y reportes. Mira la tabla y tradúcela con tu equipo. 🗝️
En PyMarket CRM Mini LMS Sistema PQRS
usuarios.json + roles Asesores, gerente, clientes Estudiantes, docentes, coordinador Ciudadanos, funcionarios, supervisor
productos.json Clientes / oportunidades Cursos y actividades Peticiones, quejas, reclamos
ventas.json Interacciones / ventas cerradas Entregas y calificaciones Respuestas y cierres de caso
id_vendedor (relación) id_asesor en cada cliente id_docente en cada curso id_funcionario en cada caso
carrito (dato temporal) Cotización en armado Intento de quiz en curso Borrador de la petición
ver_ventas_generales() Reporte de ventas del mes Reporte de notas del curso Casos resueltos vs pendientes
🧩
Receta para arrancar en equipo: ① Definan sus 3 archivos JSON con sus campos (el "plano" del Paso 1) — esta decisión define TODO lo demás. ② Dibujen el diagrama general del flujo (su algoritmo en diagrama: menús → servicios → datos, como el mapa del Paso 0). ③ Creen las carpetas menus/, services/, utils/, data/ y copien su file_manager.py — esa utilidad es igual en los 3 proyectos. ④ Repartan los servicios entre los integrantes según sus roles: como cada servicio es un archivo independiente, pueden programar en paralelo sin pisarse.
👤
Estudiante
¿Y qué es exactamente lo que tenemos que entregar?
DA
DANI · Asistente de clase
Cuatro entregables por equipo de 5. Úsenlos como checklist y asignen un responsable a cada uno — aunque todo el equipo responde por todo en el demo day. 📋
1. Algoritmo general en diagrama — el flujo básico del sistema (como el mapa del Paso 0 + el flujo de datos): menús, decisiones por rol y qué servicio toca qué archivo JSON
2. Diapositivas — problema que resuelven, roles del sistema, demo del flujo principal, estructura por capas y qué aprendió el equipo
3. Preparación demo day — ensayen el flujo completo con datos de prueba realistas; tengan un plan B si algo falla en vivo (¡y recuerden el try/except!)
4. Documentación — un README.md (qué hace el sistema, cómo ejecutarlo con main.py, estructura de carpetas, roles e integrantes) + un documento libre con lo que decidieron y por qué
🎤
Consejo de demo day: el jurado siempre pregunta "¿qué pasa si escribo letras donde van números?" y "¿qué pasa si el archivo está vacío?". Ustedes ya saben responder ambas con código: try/except ValueError y el return [] del file_manager. Practiquen provocar esos errores a propósito en el ensayo.

// Resumen de la sesión

Hoy completaste PyMarket de punta a punta. Entendiste la capa de datos: tres JSON que son listas de diccionarios relacionadas por ids (como llaves foráneas). Construiste main.py con su if __name__ == "__main__": y el menú principal que enruta por rol. En usuario_service viste funciones puras que se reutilizan entre sí y el retorno múltiple return True, mensaje. En producto_service dominaste el CRUD completo con el patrón leer → buscar → validar → actuar → guardar, list comprehensions, try/except ValueError y la seguridad por dueño. En venta_service aprendiste que el carrito vive en RAM y viaja por referencia, que carrito.copy() evita perder la venta, y que una compra es una transacción que guarda dos archivos. Y con el admin viste que los roles son, al final, filtros.

Lo más importante: ya no estás copiando código, estás reconociendo patrones. Tu CRM, mini LMS o PQRS usa exactamente estas piezas con otros nombres. Definan sus JSON, dibujen su diagrama, repartan los servicios — y a construir. Nos vemos en el demo day. 🚀

Scroll to Top