diff --git a/README.md b/README.md index 9b47c41..472e5cc 100644 --- a/README.md +++ b/README.md @@ -1,963 +1,148 @@ -# Studymation — Documentación Técnica +# Studymation -Generador de documentos académicos con IA para estudiantes mexicanos. El sistema conduce al usuario a través de un flujo conversacional donde extrae los datos del trabajo solicitado y produce un archivo `.docx` formateado con citas académicas verificadas. Monetización mediante suscripciones (Pro/Max) y créditos de prepago, ambos procesados por Stripe. +A Mexican student pastes their assignment brief and the teacher's rubric. Studymation turns +that into a finished, formatted **Word document** — institutional cover page, structured +sections, and citations that were actually looked up in academic databases rather than +invented by a language model. ---- - -## Índice - -1. [Stack técnico](#1-stack-técnico) -2. [Arquitectura general](#2-arquitectura-general) -3. [Estructura de directorios](#3-estructura-de-directorios) -4. [Configuración y variables de entorno](#4-configuración-y-variables-de-entorno) -5. [Base de datos y modelos ORM](#5-base-de-datos-y-modelos-orm) -6. [API REST](#6-api-rest) -7. [Capa LLM](#7-capa-llm) -8. [Pipeline de generación de documentos](#8-pipeline-de-generación-de-documentos) -9. [Sistema de citas académicas](#9-sistema-de-citas-académicas) -10. [Autenticación y sesiones](#10-autenticación-y-sesiones) -11. [Planes, créditos y facturación](#11-planes-créditos-y-facturación) -12. [Almacenamiento (DigitalOcean Spaces)](#12-almacenamiento-digitalocean-spaces) -13. [Frontend](#13-frontend) -14. [Comandos de desarrollo](#14-comandos-de-desarrollo) -15. [Testing](#15-testing) -16. [Despliegue en producción](#16-despliegue-en-producción) - ---- - -## 1. Stack técnico - -### Backend -| Componente | Tecnología | -|---|---| -| Framework | FastAPI 0.115 | -| Runtime | Python 3.11 | -| Base de datos | PostgreSQL 16 (async) | -| ORM | SQLAlchemy 2.0 + asyncpg | -| Migraciones | Alembic | -| Generación .docx | python-docx | -| Autenticación | JWT (python-jose) + bcrypt + Google OAuth | -| Storage | DigitalOcean Spaces (S3-compatible, boto3) | -| Pagos | Stripe (suscripciones + créditos) | -| Cache / Rate limit | Redis 7 | -| Logging | structlog (JSON en prod, consola en dev) | -| Validación | Pydantic v2 + pydantic-settings | -| Linting | ruff + mypy | -| Testing | pytest + pytest-asyncio | - -### LLM / Proveedores externos -| Proveedor | Uso | -|---|---| -| OpenAI | Generación de estructura, contenido y extracción conversacional (activo) | -| DeepSeek | Proveedor alternativo por plan/tarea (configurable) | -| Anthropic | Proveedor Claude (implementado, no activo en MVP) | -| Semantic Scholar | Citas académicas (fuente primaria) | -| Crossref | Metadatos DOI (fuente secundaria) | -| Brave Search | Fallback de citas (búsqueda web) | - -### Frontend -| Componente | Tecnología | -|---|---| -| Framework | Next.js 15 (App Router) | -| UI Library | React 19 | -| Tipado | TypeScript 5 | -| Estilos | Tailwind CSS + Radix UI | -| Animaciones | Framer Motion | -| HTTP | Axios + interceptor global de 401 | -| Forms | React Hook Form + Zod | - -### Infraestructura -| Componente | Tecnología | -|---|---| -| Contenedores | Docker multi-stage (dev y prod) | -| Orquestación | Docker Compose | -| Servidor | DigitalOcean VPS (2GB RAM) | -| Archivos | DigitalOcean Spaces | -| CI/CD | Manual (sin GitHub Actions) | - ---- - -## 2. Arquitectura general - -``` -┌─────────────────────────────────────────────────────┐ -│ FRONTEND │ -│ Next.js 15 · App Router · React 19 · TS │ -│ Conversación ─► Extracción ─► Generación ─► .docx │ -│ Planes / Créditos ─► Stripe Checkout │ -└────────────────────────┬────────────────────────────┘ - │ HTTPS / Cookie HttpOnly - │ NEXT_PUBLIC_API_URL -┌────────────────────────▼────────────────────────────┐ -│ BACKEND (API) │ -│ FastAPI · Python 3.11 · async │ -│ │ -│ /auth ──► AuthService ──► JWT / Google OAuth │ -│ /documents ──► ConversationExtractor ──► LLM │ -│ └──► DocumentGenerationPipeline │ -│ ├─ StructureStep (LLM) │ -│ ├─ ContentStep (LLM, 3 fases) │ -│ ├─ CitationStep (cascade) │ -│ └─ AssemblyStep (python-docx) │ -│ /payments ──► Stripe (suscripciones + créditos) │ -│ /admin ──► SubscriptionService / UsageLogs │ -└─────┬──────────┬────────────┬──────────┬────────────┘ - │ │ │ │ - PostgreSQL DigitalOcean OpenAI / Redis - (async) Spaces (.docx) DeepSeek (rate limit) - Anthropic -``` - ---- - -## 3. Estructura de directorios +![The guided brief: topic, subject, student data, academic context and the teacher's rubric pasted in full, with a readiness panel showing 7 of 7 required fields](docs/media/brief.png) -``` -studymation/ -├── backend/ -│ ├── app/ -│ │ ├── api/v1/ -│ │ │ ├── auth.py # Rutas /auth -│ │ │ ├── documents.py # Rutas /conversations y /documents -│ │ │ ├── payments.py # Rutas /payments (Stripe checkout + webhook) -│ │ │ └── admin.py # Rutas /admin -│ │ ├── core/ -│ │ │ ├── attachments/ # Parsers: PDF, DOCX, MD, TXT -│ │ │ ├── citations/ # Pipeline de citas (cascade + caché) -│ │ │ ├── conversation/ # ConversationExtractor -│ │ │ ├── document/ -│ │ │ │ └── pipeline/ # 4 steps de generación -│ │ │ ├── llm/ # Providers (OpenAI, DeepSeek, Anthropic) + factory -│ │ │ └── storage/ # S3Provider + TTL manager -│ │ ├── middleware/ -│ │ │ ├── auth.py # get_current_user / get_current_admin -│ │ │ ├── plan_guard.py # require_plan() dependency -│ │ │ └── rate_limit.py # Rate limiting Redis/memory (por plan e IP) -│ │ ├── models/ # ORM SQLAlchemy -│ │ │ ├── user.py -│ │ │ ├── subscription.py -│ │ │ ├── document.py -│ │ │ ├── conversation.py -│ │ │ ├── citation_cache.py -│ │ │ ├── usage_log.py -│ │ │ ├── credit_package.py # Paquetes de créditos disponibles -│ │ │ └── credit_transaction.py # Ledger inmutable de créditos -│ │ ├── schemas/ # Pydantic request/response schemas -│ │ ├── services/ -│ │ │ ├── auth_service.py -│ │ │ ├── document_service.py -│ │ │ ├── subscription_service.py -│ │ │ └── credit_service.py # Balance + transacciones atómicas -│ │ ├── utils/ -│ │ │ ├── exceptions.py # StudymationError hierarchy -│ │ │ └── logger.py # structlog setup -│ │ ├── config.py # Settings (pydantic-settings) incl. Stripe + FX -│ │ ├── database.py # AsyncEngine + session factory -│ │ └── main.py # Entry point FastAPI -│ ├── alembic/versions/ # Migraciones versionadas -│ ├── schools/ # Plantillas institucionales (CETI, etc.) -│ │ └── / -│ │ └── school.json # Metadata: logo, colores, formato portada -│ ├── tests/ -│ │ ├── unit/ -│ │ ├── integration/ -│ │ └── e2e/ -│ ├── pyproject.toml -│ ├── Dockerfile -│ └── .env.example -│ -├── frontend/ -│ ├── src/ -│ │ ├── app/ -│ │ │ ├── (app)/dashboard/ # Rutas autenticadas -│ │ │ │ ├── generar/ # Flujo conversacional principal -│ │ │ │ ├── documentos/ # Historial de documentos -│ │ │ │ ├── creditos/ # Marketplace de créditos (Stripe) -│ │ │ │ └── cuenta/ # Perfil + gestión de suscripción -│ │ │ ├── (auth)/ # login/, registro/ -│ │ │ ├── admin/ # Panel admin -│ │ │ └── auth/google/callback/ -│ │ ├── components/ -│ │ │ ├── admin/ -│ │ │ ├── conversation/ # Burbujas, progreso de extracción -│ │ │ ├── document/ # Modal disclaimer, pantalla generando -│ │ │ └── ui/ # Toast, primitivos -│ │ ├── hooks/ -│ │ │ ├── useAuth.ts -│ │ │ ├── usePlan.ts -│ │ │ └── useCredits.ts # Balance de créditos -│ │ └── lib/ -│ │ ├── api.ts # Axios instance + namespaced clients -│ │ ├── plans.ts # Definición de planes y precios -│ │ └── utils.ts -│ ├── package.json -│ ├── next.config.ts -│ ├── tailwind.config.ts -│ └── Dockerfile -│ -├── docker-compose.yml # Entorno de desarrollo -├── docker-compose.prod.yml # Entorno de producción -├── Makefile # +60 comandos -└── .env # Variables globales (gitignored) -``` +The brief is the product's contract with the student. The rubric is not a "notes" field: +it is parsed into a list of deliverables that the pipeline must satisfy and that a later +step audits. Attachments (the PDF the teacher handed out) are parsed and used the same way. --- -## 4. Configuración y variables de entorno - -Toda configuración del backend pasa por `app/config.py` usando `pydantic-settings`. **Nunca** leer `os.environ` directamente en el código; siempre importar `settings`. En tests, llamar `get_settings.cache_clear()` antes de sobreescribir variables. - -### Variables del backend (`.env`) - -```bash -# ── Aplicación ──────────────────────────────────────────── -APP_ENV=development # development | staging | production -DEBUG=true -ALLOWED_ORIGINS=http://localhost:3000 -FRONTEND_URL=http://localhost:3000 - -# ── LLM (config por plan + tarea) ───────────────────────── -# Formato nuevo (tiene precedencia sobre legacy) -LLM_{PLAN}_{TASK}_PROVIDER=openai # openai | deepseek | anthropic -LLM_{PLAN}_{TASK}_MODEL=gpt-4.1-mini - -# Planes: free | pro | max -# Tareas: structure | content | conversation | citations - -# Fallback legacy (si no hay config por tarea) -LLM_PROVIDER=openai -LLM_MODEL_FREE=gpt-4.1-mini -LLM_MODEL_PRO=gpt-4.1-mini -LLM_MODEL_MAX=gpt-4.1-mini - -# ── API Keys ────────────────────────────────────────────── -OPENAI_API_KEY=sk-... -DEEPSEEK_API_KEY=... -ANTHROPIC_API_KEY=... -BRAVE_SEARCH_API_KEY=... - -# ── Base de datos ───────────────────────────────────────── -DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/studymation -DATABASE_URL_TEST=postgresql+asyncpg://user:pass@db_test:5432/studymation_test - -# ── Redis ───────────────────────────────────────────────── -REDIS_URL=redis://redis:6379/0 -RATE_LIMIT_BACKEND=memory # memory (dev) | redis (prod, multi-worker) - -# ── Storage (DigitalOcean Spaces) ───────────────────────── -STORAGE_ENDPOINT=https://nyc3.digitaloceanspaces.com -STORAGE_BUCKET=studymation-docs -STORAGE_ACCESS_KEY=... -STORAGE_SECRET_KEY=... -FREE_DOCUMENT_TTL_HOURS=72 - -# ── JWT ─────────────────────────────────────────────────── -JWT_SECRET_KEY=... # openssl rand -hex 32 -JWT_EXPIRE_DAYS=7 - -# ── Google OAuth ────────────────────────────────────────── -GOOGLE_CLIENT_ID=... -GOOGLE_CLIENT_SECRET=... - -# ── Stripe ──────────────────────────────────────────────── -STRIPE_SECRET_KEY=sk_test_... -STRIPE_WEBHOOK_SECRET=whsec_... - -# ── Billing / Créditos ──────────────────────────────────── -USD_TO_MXN=17.44 # Actualizar periódicamente -USD_TO_MXN_LAST_UPDATED=2026-05-08 # Si >7 días, Settings.is_fx_stale() = true -PROFIT_MARGIN_PCT=300 # Margen sobre costo LLM (300 = 3x) -MAX_NEGATIVE_BALANCE=-20.0 # Balance mínimo permitido en créditos - -# ── Crossref (buenas prácticas de la API) ───────────────── -CROSSREF_MAILTO=tu@studymation.online -``` - -### Variables del frontend (`.env.local`) - -```bash -NEXT_PUBLIC_API_URL=http://localhost:8000 -# En producción: https://api.studymation.online -``` - -### Resolución de modelo LLM - -`Settings.get_task_provider_model(plan, task)` resuelve en este orden: - -1. `LLM_{PLAN}_{TASK}_PROVIDER` + `LLM_{PLAN}_{TASK}_MODEL` (específico por plan y tarea) -2. `LLM_PROVIDER` + `LLM_MODEL_{PLAN}` (legacy, fallback) - ---- - -## 5. Base de datos y modelos ORM - -PostgreSQL 16 con SQLAlchemy 2.0 async (asyncpg). Todas las conexiones son asíncronas. El `pool_size` es 5 con `max_overflow` 10. - -### Modelos - -#### `users` -``` -id UUID PK -email TEXT UNIQUE NOT NULL (indexed) -password_hash TEXT NULL (null si es Google OAuth) -google_id TEXT UNIQUE NULL -full_name TEXT NOT NULL -is_active BOOL DEFAULT true -is_admin BOOL DEFAULT false -created_at TIMESTAMPTZ -updated_at TIMESTAMPTZ -``` - -#### `subscriptions` -``` -id UUID PK -user_id UUID FK → users (indexed) -plan ENUM(free, pro, max) -status ENUM(active, suspended, cancelled) -activated_by UUID FK → users NULL (admin que activó o Stripe) -activation_note TEXT NULL (referencia de pago) -stripe_session_id TEXT NULL (Stripe Checkout Session ID) -started_at TIMESTAMPTZ -expires_at TIMESTAMPTZ NULL (null = sin expiración) -``` - -#### `documents` -``` -id UUID PK -user_id UUID FK → users (indexed) -title TEXT NOT NULL -document_type TEXT NOT NULL -school_id TEXT NOT NULL -page_count INT -has_cover_page BOOL -storage_key TEXT NULL (ruta en Spaces) -storage_expires TEXT NULL (TTL para plan Free) -generation_status ENUM(pending, generating, completed, failed) -llm_model_used TEXT (auditoría de costos) -citations_count INT -metadata_ JSONB -error_detail TEXT NULL -created_at TIMESTAMPTZ -updated_at TIMESTAMPTZ -``` - -#### `conversations` -``` -id UUID PK -user_id UUID FK → users (indexed) -document_id UUID FK → documents NULL -status ENUM(active, completed, abandoned) -extracted_data JSONB (campos acumulados por turno) -messages JSONB[] (historial completo de mensajes) -created_at TIMESTAMPTZ -updated_at TIMESTAMPTZ -``` - -#### `citations_cache` -``` -id UUID PK -query_hash TEXT UNIQUE (SHA256 de la query normalizada) -result JSONB (cita verificada o found=false) -provider TEXT (semantic_scholar | crossref | brave) -created_at TIMESTAMPTZ (TTL 30 días) -``` - -#### `usage_logs` -``` -id UUID PK -user_id UUID FK → users -document_id UUID FK → documents NULL -action TEXT (generate | download) -model TEXT -tokens INT -cost_usd NUMERIC(10,6) -created_at TIMESTAMPTZ -``` - -#### `credit_packages` -``` -id UUID PK -code TEXT UNIQUE (ej. "starter", "professional", "enterprise") -name TEXT -credits NUMERIC (cantidad de créditos) -price_usd NUMERIC(10,4) -stripe_price_id TEXT NULL -active BOOL DEFAULT true -created_at TIMESTAMPTZ -``` - -#### `credit_transactions` -``` -id UUID PK -user_id UUID FK → users (indexed) -type ENUM(purchase, consume, refund, bonus) -amount NUMERIC (positivo = ingreso, negativo = consumo) -reference_id TEXT UNIQUE NULL (idempotency key — ej. stripe_session_id) -description TEXT NULL -document_id UUID FK → documents NULL -created_at TIMESTAMPTZ -``` +## From brief to .docx -### Migraciones +![Pipeline: understand the assignment and its rubric, write in phases, verify citations against real academic sources, and deliver a .docx](docs/media/pipeline.svg) -```bash -make migrate # Aplicar pendientes (alembic upgrade head) -make migrate-create msg="mensaje" # Auto-generar desde modelos ORM -make shell # bash en contenedor para correr alembic manual -``` +The pipeline is not one giant prompt. Each stage is a step with its own model +configuration, its own failure mode and its own audit trail — and **which steps run at all +depends on what was asked for**. A `GenerationProfile` derived from the document type +decides whether the document needs a thesis plan, an introduction, a conclusion, an +overlap check between sections, or citations. A one-page answer and a research essay go +through genuinely different pipelines, not the same pipeline with a different word count. ---- +![Generation screen showing the real backend state: analysis and structure completed, content generation in progress](docs/media/generacion.png) -## 6. API REST - -Base path: `/api/v1`. Todas las rutas de documentos y admin requieren autenticación JWT. - -### Auth (`/api/v1/auth`) - -| Método | Ruta | Descripción | -|---|---|---| -| POST | `/auth/register` | Registro con email + password. Auto-activa plan Free. | -| POST | `/auth/login` | Login. Setea cookie HttpOnly. | -| POST | `/auth/logout` | Limpia cookie HttpOnly. | -| GET | `/auth/google` | Redirige a Google OAuth. | -| GET | `/auth/google/callback` | Callback OAuth. Crea usuario si es nuevo. | -| GET | `/auth/me` | Datos del usuario autenticado. | -| GET | `/auth/me/subscription` | Plan activo: `{ plan, is_trial, expires_at }`. | - -### Documentos (`/api/v1`) - -| Método | Ruta | Descripción | -|---|---|---| -| POST | `/conversations` | Inicia nueva conversación vacía. | -| POST | `/conversations/{id}/turn` | Envía turno: `{ message, files? }`. Retorna pregunta siguiente. | -| POST | `/conversations/{id}/generate` | Dispara el pipeline de generación. | -| GET | `/documents` | Historial de documentos del usuario. | -| GET | `/documents/{id}/citation-audit` | Razones de aceptación/rechazo de citas por sección. | -| GET | `/documents/{id}` | Detalle de un documento del usuario. | -| GET | `/documents/{id}/status` | Estado de generación del documento (`pending`, `processing`, `completed`, `failed`). | -| GET | `/documents/{id}/download` | Genera signed URL de Spaces + registra UsageLog. | -| POST | `/conversations/{id}/direct-submit` | Envía todos los campos de la conversación de una vez (sin turnos). | -| POST | `/documents/{id}/disclaimer-accepted` | Confirma que el usuario revisó el documento. | -| GET | `/schools` | Lista de plantillas institucionales disponibles. | - -### Pagos (`/api/v1/payments`) — requiere autenticación - -| Método | Ruta | Descripción | -|---|---|---| -| POST | `/payments/create-checkout-session` | Stripe Checkout para compra de paquete de créditos. | -| POST | `/payments/create-plan-checkout` | Stripe Checkout para suscripción Pro o Max. | -| POST | `/payments/stripe/webhook` | Webhook Stripe (firma validada). Activa plan o acredita créditos. | - -### Admin (`/api/v1/admin`) — requiere `is_admin=true` - -| Método | Ruta | Descripción | -|---|---|---| -| GET | `/admin/users` | Lista todos los usuarios. | -| GET | `/admin/users/{id}` | Detalle de un usuario. | -| PATCH | `/admin/users/{id}` | Modificar `is_active` o `is_admin`. | -| POST | `/admin/subscriptions` | Activar plan manualmente. | -| GET | `/admin/subscriptions/{id}` | Suscripción activa de un usuario. | -| GET | `/admin/stats/costs` | Estadísticas de tokens y costo USD total y por usuario/modelo. | -| GET | `/admin/users/{id}/costs` | Desglose de costos por documento de un usuario. | -| POST | `/admin/maintenance/cleanup` | Elimina documentos Free con TTL expirado de Spaces. | - -### Respuestas de error - -Todos los errores del dominio heredan de `StudymationError(detail, code)`. El `code` es lo que el frontend usa programáticamente. El handler global mapea `code` a status HTTP. - -```python -# Autenticación -AuthenticationError → 401 -TokenExpiredError → 401 -InsufficientPermissionsError → 403 - -# Usuarios -UserNotFoundError → 404 -UserAlreadyExistsError → 409 - -# Planes / Créditos -PlanLimitExceededError → 429 -FeatureNotAvailableError → 403 -InsufficientCreditsError → 402 -InvalidUploadError → 413 - -# Documentos -ConversationNotReadyError → 400 -DocumentNotFoundError → 404 -DocumentExpiredError → 410 -DocumentGenerationError → 500 - -# Citas -CitationNotVerifiedError → 500 - -# Instituciones -SchoolNotFoundError → 404 -``` +The progress screen is not an animation: it polls the document's real `current_step`, so +what you see is the stage the backend is actually on. --- -## 7. Capa LLM - -### Providers (`app/core/llm/`) - -- `BaseLLMProvider` — interfaz común (método `complete(messages, model, ...)`) -- `OpenAIProvider` — wrapper async para OpenAI SDK -- `DeepSeekProvider` — compatible con API OpenAI (base URL diferente) -- `AnthropicProvider` — wrapper para SDK Anthropic (Claude Opus/Sonnet/Haiku); implementado pero no activo en MVP -- `factory.py` — `get_llm_for_plan(plan, task)` devuelve instancia del provider + nombre del modelo - -### Configuración por plan y tarea - -El sistema soporta configuración granular vía env vars. Ejemplo para que Free use DeepSeek en generación de contenido pero OpenAI en conversación: +## The deliverable -```bash -LLM_FREE_CONTENT_GENERATION_PROVIDER=deepseek -LLM_FREE_CONTENT_GENERATION_MODEL=deepseek-chat -LLM_FREE_CONVERSATION_EXTRACTION_PROVIDER=openai -LLM_FREE_CONVERSATION_EXTRACTION_MODEL=gpt-4.1-mini -``` - -Si no se define una combinación plan+tarea, cae al legacy `LLM_PROVIDER` + `LLM_MODEL_{PLAN}`. +![Two pages of a generated document: the CETI institutional cover page with the student's data, and the body with headings, an in-text citation and the references section](docs/media/documento.png) -### ContentStep — arquitectura de 3 fases +Word is the format Mexican schools ask for, so the output is a real `.docx` built with +python-docx: cover page from the school's own template, heading styles, tables, charts and +diagrams when the document calls for them, in-text citations and an APA 7 reference list +that only contains sources marked as used. -`ContentStep` implementa un pipeline de calidad en 3 fases para evitar repetición y asegurar coherencia: - -| Fase | Descripción | Modelo | -|---|---|---| -| 0 — Planificación global | Define tesis del documento + 3-4 afirmaciones por sección | Nano (mini) | -| 1 — Desarrollo de secciones | Genera cada sección con contexto enriquecido (afirmaciones, temas a evitar, secciones adyacentes) | Configurado por plan | -| 2 — Intro y conclusión | Generados después del desarrollo, usando resúmenes reales de cada sección | Configurado por plan | - -Validaciones adicionales: -- Longitud mínima por tipo de sección (Intro: 100 chars, Secciones: 150 chars, Conclusión: 100 chars) -- Detección de solapamiento semántico entre secciones adyacentes (Jaccard ≥ 0.40 → autocorrección, máx. 1 llamada extra) +*(The document above was produced by running the assembler step with the repository's own +test fixtures — cover, styles, citation and reference page are the system's output; the +prose is fixture text, since this environment has no LLM keys.)* --- -## 8. Pipeline de generación de documentos - -`DocumentGenerationPipeline` en `app/core/document/pipeline/` ejecuta 4 pasos de forma secuencial: +## What makes it more than a prompt wrapper -### Paso 1 — StructureStep -- Llama al LLM con el `extracted_data` de la conversación -- Devuelve un outline JSON con intención, keywords, conceptos y queries de cita por sección -- Usa plantilla institucional de `schools//school.json` para adaptar formato y portada +**Citations are found, ranked and validated — or reported as missing.** Candidates come +from Semantic Scholar, Crossref and Brave, are deduplicated, scored against the section +they would support, and revalidated even when they come from cache. A DOI that resolves is +only one signal; relevance decides. If nothing survives, the section is recorded as +`found=False` with the rejection reasons and the reference list does not grow. The audit +is stored with the document and exposed at +`GET /api/v1/documents/{id}/citation-audit`. -### Paso 2 — ContentStep (3 fases) -- Fase 0: planificación global con modelo nano (+1 llamada LLM por documento) -- Fase 1: secciones en paralelo con contexto enriquecido -- Fase 2: intro y conclusión en paralelo, generados a partir de los resúmenes reales -- Respeta el límite de páginas del plan del usuario +**The rubric is a contract.** `ContractValidationStep` re-reads the finished content +against the deliverables extracted from the rubric and logs auditable warnings +(`contract_deliverable_missing`, `contract_deliverable_constraints_violated`, +`contract_duplicate_sections`) +instead of silently shipping a document that ignored half the assignment. -### Paso 3 — CitationStep -- Por cada sección con intención de cita, busca candidatos académicos y los rankea por relevancia -- Usa `CitationPipeline` (ver sección 9) -- Inserta citas en texto de forma determinista; la bibliografía solo incluye fuentes usadas -- Si no encuentra cita útil, declara `found=False` explícitamente (nunca inventa ni infla referencias) +**The quality gate never blocks delivery.** A terminal step scans the rendered `.docx` for +artifacts a student would notice — raw Mermaid, unresolved placeholders, stray markdown, +charts without provenance — and reports them. The student still gets the file; the issue +gets logged. -### Paso 4 — AssemblyStep -- Recibe estructura + contenido + citas -- Construye el `.docx` con python-docx -- Aplica estilos institucionales: portada (logo, colores del `school.json`), cuerpo con markdown-lite, tablas, listas, blockquotes -- Intenta insertar imágenes reales vía Brave con fallback a diagrama Mermaid si no se encuentra imagen válida - -### Orquestación en `document_service.py` - -``` -1. Verificar límites del plan (SubscriptionService) -2. Crear registro Document (status: PENDING → GENERATING) -3. Ejecutar DocumentGenerationPipeline -4. Subir .docx a DigitalOcean Spaces -5. Actualizar Document (status: COMPLETED, storage_key, citations_count) -6. Registrar UsageLog (tokens, cost_usd) -``` +**Content is written in phases.** Global plan (thesis and claims per section), sections in +parallel with awareness of their neighbours, then introduction and conclusion written from +what the sections actually say, then a Jaccard overlap check that rewrites sections that +say the same thing twice. --- -## 9. Sistema de citas académicas - -`CitationPipeline` en `app/core/citations/` implementa búsqueda multi-proveedor, -deduplicación, scoring semántico y auditoría con caché en PostgreSQL. - -### Flujo de ranking - -``` -Contexto de sección - │ - ▼ -Queries propias + topic + conceptos clave - │ - ▼ -Cache + Semantic Scholar + Crossref + Brave - │ - ▼ -Normalización + deduplicación por DOI/título - │ - ▼ -CitationRanker - │ score >= umbral - ▼ -Cita insertada en texto + referencia APA 7 - │ score bajo - ▼ -found=False con razones de rechazo -``` - -### Caché - -- Clave: SHA256 de la query normalizada -- TTL configurable con `CITATION_CACHE_TTL_DAYS` -- Tabla: `citations_cache` -- Los resultados cacheados se revalidan contra la sección actual antes de aceptarse - -### Validación y formato +## Around the pipeline -- `CitationRelevanceValidator` separa DOI válido de cita académica útil -- `CitationRanker` usa relevancia título/query/sección/contenido, metadatos, proveedor y penalizaciones -- Rechaza coincidencias superficiales, títulos genéricos, duplicados, fuentes sin metadatos y resultados fuera de dominio -- `CitationFormatter` formatea la salida en APA 7 -- `GET /api/v1/documents/{id}/citation-audit` expone razones de aceptación/rechazo +| Layer | What is there | +| --- | --- | +| Accounts | Email + password or Google OAuth, JWT in an HttpOnly cookie | +| Plans | `free`, `pro`, `max` — plan-gated features, per-plan rate limits and per-plan LLM models | +| Credits | 1 credit = 1 MXN, immutable ledger, `SELECT FOR UPDATE` on balance, idempotent by reference id | +| Payments | Stripe Checkout for both subscriptions and credit packs, webhook with signature validation | +| Schools | Each institution is a folder (`backend/schools//school.json`) with its own cover format, colours and logo | +| Attachments | `.txt`, `.md`, `.pdf`, `.docx` parsed through a plugin registry, size- and count-limited | +| Storage | DigitalOcean Spaces (S3-compatible); free-plan documents expire and are swept by a maintenance endpoint | +| Admin | Users, subscriptions and per-generation cost tracking | --- -## 10. Autenticación y sesiones +## Stack and shape -### Flujo email + password +FastAPI (async, Python 3.11) + PostgreSQL through SQLAlchemy 2.0 and Alembic, Redis for +rate limiting, Next.js 15 + React 19 for the frontend, Docker Compose for both. LLM access +goes through a provider-agnostic layer (OpenAI, DeepSeek, Anthropic) resolved **per plan +and per task**, so structure generation, content generation, rubric extraction and JSON +repair can each run on a different model. ``` -POST /auth/register - → hash bcrypt - → crear User - → auto-activar plan Free (trial 3 meses) - → crear JWT (exp: JWT_EXPIRE_DAYS) - → Set-Cookie: studymation_token (HttpOnly, Secure en prod) - -POST /auth/login - → verificar bcrypt - → crear JWT - → Set-Cookie +backend/app/ +├── api/ # routers: auth · documents · payments · admin (/api/v1) +├── core/ +│ ├── document/ # pipeline (structure → content → contract → citations → assembly → gate) +│ ├── citations/ # candidate search, dedup, ranking, validation, cache +│ ├── llm/ # provider-agnostic layer + prompts +│ ├── attachments/# parser registry per file type +│ └── storage/ # S3-compatible provider +├── models/ schemas/ services/ middleware/ +frontend/src/app/ +├── (app)/dashboard/ # generate · documents · credits · account +├── (auth)/ # login · registro +└── admin/ # users · subscriptions · costs ``` -### Flujo Google OAuth - -``` -GET /auth/google - → guardar state en cookie temporal (10 min, SameSite=lax) - → redirect a accounts.google.com - -GET /auth/google/callback?code=...&state=... - → validar state - → intercambiar code por tokens Google - → si usuario nuevo: crear con google_id - → si existe: actualizar google_id - → crear JWT → Set-Cookie -``` - -### Verificación de sesión - -Todas las rutas protegidas usan la dependencia `get_current_user()`: - -``` -Prioridad de token: - 1. Header Authorization: Bearer - 2. Cookie studymation_token - 3. 401 si ninguno presente o expirado -``` - -### Middleware de plan - -`require_plan(["pro", "max"])` es una dependency factory que verifica el plan activo del usuario antes de procesar la request. Devuelve `FeatureNotAvailableError (403)` si el plan no es suficiente. - --- -## 11. Planes, créditos y facturación +## Run it locally -### Planes de suscripción - -| Plan | Precio MXN | Docs/mes | Docs/semana | Máx páginas | Almacenamiento | -|---|---|---|---|---|---| -| **Free** | $0 | 2 | — | 3 | 72 h TTL | -| **Pro** | $99/mes | — | 15 | 20 | Permanente | -| **Max** | $149/mes | ∞ | ∞ | ∞ | Permanente | - -La lógica de límites está en `app/services/subscription_service.py`. La definición de precios y features para el frontend está en `frontend/src/lib/plans.ts`. - -### Activación de planes vía Stripe - -Los planes Pro y Max se activan mediante Stripe Checkout: - -``` -Frontend → POST /payments/create-plan-checkout - → Stripe crea sesión de pago - → Usuario completa pago en Stripe - → Stripe → POST /payments/stripe/webhook (checkout.session.completed) - → Backend activa suscripción con stripe_session_id como referencia -``` - -También pueden activarse manualmente desde el panel admin (ej. para pagos SPEI o bonificaciones). - -### Sistema de créditos - -1 crédito = 1 MXN. Los créditos cubren el costo de LLM con un margen configurado (`PROFIT_MARGIN_PCT`). - -**Paquetes disponibles** (configurables en tabla `credit_packages`): -- Starter: 100 créditos -- Professional: 500 créditos -- Enterprise: 1000+ créditos - -**Flujo de compra:** -``` -Frontend → POST /payments/create-checkout-session { package_code } - → Stripe crea sesión - → Usuario paga - → Stripe webhook → CreditService.add_credits() con stripe_session_id como reference_id -``` - -**Garantías del sistema de créditos:** -- Transacciones atómicas: `SELECT FOR UPDATE` en tabla de créditos -- Idempotencia: `reference_id` único evita acreditaciones duplicadas -- Balance negativo permitido hasta `MAX_NEGATIVE_BALANCE` (default: -20.0) -- El backend es source of truth — el frontend no puede alterar precios ni cantidades - -**Tipo de cambio:** `USD_TO_MXN` en `.env`. Si `Settings.is_fx_stale()` detecta que la fecha `USD_TO_MXN_LAST_UPDATED` tiene más de 7 días, el endpoint de checkout emite advertencia en logs. - -### Rate limiting - -| Contexto | Límite | -|---|---| -| Free | 10 requests/min | -| Pro | 30 requests/min | -| Max | 60 requests/min | -| Auth (login/register) | 10 intentos/min por IP | - -Configurar `RATE_LIMIT_BACKEND=redis` en producción para que el límite sea compartido entre workers. - ---- - -## 12. Almacenamiento (DigitalOcean Spaces) - -`app/core/storage/s3_provider.py` usa boto3 con la API S3-compatible de Spaces. - -### Operaciones principales - -| Operación | Descripción | -|---|---| -| `upload_document(bytes, key)` | Sube `.docx` al bucket | -| `generate_presigned_url(key, expires_in)` | URL de descarga firmada (tiempo limitado) | -| `delete_document(key)` | Elimina archivo del bucket | - -### TTL de documentos Free - -Los documentos del plan Free tienen una expiración de `FREE_DOCUMENT_TTL_HOURS` (default 72 h). La limpieza se ejecuta manualmente: +Everything runs through `make`; the dev stack is Docker Compose (API, PostgreSQL, Redis). ```bash -make cleanup-docs -# o -POST /api/v1/admin/maintenance/cleanup -``` - ---- +make up-detached # start the stack +make migrate # apply migrations +make fe-dev # Next.js dev server with hot reload +make dev # both at once -## 13. Frontend - -### Cliente API (`src/lib/api.ts`) - -Axios instance con `withCredentials: true` para enviar automáticamente la cookie HttpOnly. Interceptor global que redirige a `/login` en respuestas 401. - -```typescript -// Clients disponibles -authApi // register, login, logout, me, mySubscription -documentsApi // startConversation, sendTurn, generate, listDocuments, download, listSchools -paymentsApi // createCheckoutSession, createPlanCheckout -adminApi // listUsers, getUser, updateUser, activatePlan, getCostStats, ... +make test # pytest, excluding tests that call external APIs +make check # ruff + mypy ``` -Nunca usar `fetch` directo en componentes; siempre pasar por estos clients. - -### Hooks principales - -**`useAuth`** — verifica sesión en mount con `GET /auth/me`. Expone `{ user, loading, isAuthenticated, logout, refetch }`. - -**`usePlan`** — llama `GET /auth/me/subscription` cuando `isAuthenticated=true`. Expone `{ plan, loading, is_trial, expires_at }`. +The backend needs a `.env`; at minimum a database URL and a JWT secret. LLM, citation, +Stripe and storage keys are optional in development — without them the app runs, but +document generation stops at the first LLM call. -**`useCredits`** — balance de créditos del usuario. Expone `{ balance, loading, refetch }`. - -### Rutas - -``` -/ Landing page -/login Login -/registro Registro -/auth/google/callback Callback OAuth (fuera del grupo auth) - -(autenticadas — redirigen a /login si no hay sesión) -/dashboard Home del usuario -/dashboard/generar Flujo conversacional principal -/dashboard/documentos Historial de documentos -/dashboard/creditos Marketplace de créditos (Stripe Checkout) -/dashboard/cuenta Perfil + gestión de suscripción (upgrade a Pro/Max) - -(admin — requieren is_admin=true) -/admin Panel principal -/admin/usuarios Gestión de usuarios -/admin/suscripciones Activar planes -/admin/costos Estadísticas LLM (tokens, USD) -``` - -### Flujo de generación (`/dashboard/generar`) - -``` -1. startConversation() → conversation_id -2. Loop de turnos: - sendTurn({ message, files? }) → { assistant_message, extracted_data, - ready_to_generate, missing_fields } -3. ready_to_generate = true: - generate() → Document (status: generating) - → mostrar GeneratingScreen -4. status: completed: - → mostrar DisclaimerModal -5. Usuario confirma: - download() → signed URL - → descarga automática del .docx -``` - -Los 8 campos requeridos para `ready_to_generate`: -`topic`, `document_type`, `subject`, `student_name`, `student_id`, `grade`, `group`, `teacher` +→ [Documentation index](docs/README.md) · [Setup](docs/setup.md) · [Architecture](docs/architecture.md) · +[API reference](docs/api) · [Pipeline](docs/modules/pipeline.md) · +[Citations](docs/modules/citations.md) · [Runbook](docs/runbook.md) · +[Troubleshooting](docs/troubleshooting/guide.md) · +[Referencia técnica en español](docs/referencia-tecnica.md) --- -## 14. Comandos de desarrollo - -Todos los comandos se ejecutan desde la raíz del repo con `make`. El Makefile usa `docker-compose.prod.yml` para operaciones de backend por defecto. - -### Servicios +## Academic honesty -```bash -make up-detached # Levantar servicios en background -make up # Levantar con logs en foreground -make down # Apagar servicios -make restart # Reiniciar -make status # Ver estado de contenedores -make logs # Tail de todos los logs -make logs-api # Solo logs del API -make stats # CPU y memoria de contenedores -make health # curl /health -``` - -### Base de datos - -```bash -make migrate # alembic upgrade head -make migrate-create msg="mensaje" # Auto-generar migración -make db-reset # DROP + CREATE dev DB (destructivo) -make psql # psql interactivo en contenedor -make shell # bash dentro del contenedor API -``` - -### Calidad de código - -```bash -make lint # ruff check app/ tests/ -make format # ruff format app/ tests/ -make typecheck # mypy app/ -make check # lint + typecheck -``` - -### Frontend (local) - -```bash -make fe-dev # Next.js dev server con hot-reload (npm run dev) -make fe-build # Build de producción -make fe-lint # ESLint -make dev # Backend (Docker) + Frontend (local) simultáneamente -``` - -### Admin - -```bash -make create-admin email=user@ejemplo.com # Promover usuario a admin -make cleanup-docs # Limpiar docs Free expirados -``` - ---- - -## 15. Testing - -Los tests corren **dentro del contenedor API** (`docker compose exec api pytest ...`). - -### Configuración - -- `conftest.py` crea la BD `studymation_test` al inicio de la sesión -- Cada test obtiene una transacción que hace rollback automático al terminar (sin limpieza manual) -- BD de test separada en `db_test` (puerto 5433) - -### Markers - -```python -@pytest.mark.slow # Llama APIs externas reales (Semantic Scholar, Brave, etc.) -@pytest.mark.unit # Sin BD, sin I/O externa -@pytest.mark.integration # Requiere BD de test -``` - -### Comandos - -```bash -make test # Todos excepto slow (-m "not slow") -make test-unit # Solo unit -make test-integration # Solo integration -make test-slow # Solo tests que llaman APIs externas -make test-coverage # Con reporte HTML en htmlcov/ -make test-file f=tests/unit/test_auth.py # Un archivo específico -``` - ---- - -## 16. Despliegue en producción - -### Configuración Docker Compose de producción - -`docker-compose.prod.yml` difiere del de desarrollo en: -- Sin pgAdmin, sin `db_test`, sin hot-reload -- Límites de memoria: API 800MB, DB 512MB, Frontend 512MB, Redis 64MB -- Puerto 5432 no expuesto (solo red interna Docker) -- `restart: always` en todos los servicios -- API con 2 workers uvicorn -- Redis con `RATE_LIMIT_BACKEND=redis` para seguridad multi-worker - -### Comandos de producción - -```bash -make prod-build # Construir imágenes de producción -make prod-up # Levantar servicios prod -make prod-logs # Tail logs prod -make prod-migrate # alembic upgrade head en prod -make prod-create-admin email=user@mail.com # Crear admin en prod -make prod-health # Verificar /health en prod -``` - -### Checklist de primer despliegue - -1. Copiar `.env.example` → `.env` y completar todas las variables (incluyendo Stripe keys) -2. Copiar `frontend/.env.local.example` → `frontend/.env.local` con la URL de API de prod -3. Configurar webhook en Stripe Dashboard apuntando a `https://api.studymation.online/api/v1/payments/stripe/webhook` -4. `make prod-build` -5. `make prod-up` -6. `make prod-migrate` -7. `make prod-create-admin email=...` -8. Verificar con `make prod-health` - -### Variables críticas para producción - -```bash -APP_ENV=production -DEBUG=false -# AUTH_COOKIE_SECURE se activa automáticamente cuando APP_ENV=production — no configurar explícitamente. -AUTH_COOKIE_SAMESITE=lax -ALLOWED_ORIGINS=https://studymation.online -NEXT_PUBLIC_API_URL=https://api.studymation.online -RATE_LIMIT_BACKEND=redis # Multi-worker safe -STRIPE_SECRET_KEY=sk_live_... -STRIPE_WEBHOOK_SECRET=whsec_... -POSTGRES_PASSWORD=... # Debe coincidir con la contraseña en DATABASE_URL -``` +Studymation is an assistance tool: the student accepts responsibility for the work before +generating, the document is meant to be reviewed and edited, and the system refuses to +fabricate the one thing that would be hardest to check — the sources. diff --git a/docs/backend-overview.md b/docs/backend-overview.md index 12af220..f768a55 100644 --- a/docs/backend-overview.md +++ b/docs/backend-overview.md @@ -131,7 +131,7 @@ Thin HTTP layer. Routes, Pydantic validation, dependency injection. Delegates to ### `app/models/` -SQLAlchemy ORM definitions. All primary keys are UUIDs. Timestamps are timezone-aware. See [database/models.md](../database/models.md) for full details. +SQLAlchemy ORM definitions. All primary keys are UUIDs. Timestamps are timezone-aware. See [database/models.md](database/models.md) for full details. ### `app/schemas/` @@ -151,7 +151,7 @@ The heaviest module. Contains all AI/LLM coordination, citation logic, document ### `app/utils/exceptions.py` -All domain exceptions inherit from `StudymationError(detail, code)`. The `code` string drives HTTP status mapping in `main.py`. Front-end uses `code` programmatically. See [troubleshooting/guide.md](../troubleshooting/guide.md) for the full error code table. +All domain exceptions inherit from `StudymationError(detail, code)`. The `code` string drives HTTP status mapping in `main.py`. Front-end uses `code` programmatically. See [troubleshooting/guide.md](troubleshooting/guide.md) for the full error code table. ### `app/utils/logger.py` @@ -159,7 +159,7 @@ structlog configuration. JSON in production, human-readable in development. Alwa ### `alembic/` -Alembic async configuration. 15 migrations covering the full schema evolution. `env.py` imports `Base.metadata` for autogenerate support. See [database/migrations.md](../database/migrations.md). +Alembic async configuration. 15 migrations covering the full schema evolution. `env.py` imports `Base.metadata` for autogenerate support. See [database/migrations.md](database/migrations.md). ### `schools/` diff --git a/docs/media/brief.png b/docs/media/brief.png new file mode 100644 index 0000000..2f93c8e Binary files /dev/null and b/docs/media/brief.png differ diff --git a/docs/media/documento.png b/docs/media/documento.png new file mode 100644 index 0000000..a7ddbad Binary files /dev/null and b/docs/media/documento.png differ diff --git a/docs/media/generacion.png b/docs/media/generacion.png new file mode 100644 index 0000000..cbc8a25 Binary files /dev/null and b/docs/media/generacion.png differ diff --git a/docs/media/pipeline.svg b/docs/media/pipeline.svg new file mode 100644 index 0000000..54fc4f7 --- /dev/null +++ b/docs/media/pipeline.svg @@ -0,0 +1,63 @@ + + + + FROM ASSIGNMENT TO DELIVERABLE + What happens after you paste the rubric + + + + 01 + Understand + + Topic, subject, school data and the + teacher's rubric (pasted or as a PDF) + → contract of deliverables + → profile: which steps run at all + + + + + + 02 + Write + + Structure from the school template + Content in phases: thesis and claims, + sections in parallel, then intro and + conclusion; overlap check between them + + + + + + 03 + Verify + + Citations searched in Semantic Scholar, + Crossref and Brave, then ranked and + validated against the section + Rubric contract audited, gaps logged + + + + + + 04 + Deliver + + Word assembly: cover page, styles, + in-text citations and APA 7 references + Quality gate scans the rendered file + Stored and downloaded as .docx + + + + No citation is ever invented + If no source survives validation for a claim, the pipeline records why and the reference list simply does not include it. The audit is stored with the document. + + \ No newline at end of file diff --git a/docs/referencia-tecnica.md b/docs/referencia-tecnica.md new file mode 100644 index 0000000..1051494 --- /dev/null +++ b/docs/referencia-tecnica.md @@ -0,0 +1,969 @@ +# Studymation — Referencia técnica (es) + +> Documento de referencia interno. Para la presentación del producto y el mapa de la +> documentación, empieza por el [README](../README.md). + +Generador de documentos académicos con IA para estudiantes mexicanos. El usuario describe +su trabajo y pega la rúbrica del profesor en un formulario guiado —que el backend procesa +como turnos de conversación— y el sistema produce un archivo `.docx` formateado con citas +académicas verificadas. Monetización mediante suscripciones (Pro/Max) y créditos de prepago, ambos procesados por Stripe. + +--- + +## Índice + +1. [Stack técnico](#1-stack-técnico) +2. [Arquitectura general](#2-arquitectura-general) +3. [Estructura de directorios](#3-estructura-de-directorios) +4. [Configuración y variables de entorno](#4-configuración-y-variables-de-entorno) +5. [Base de datos y modelos ORM](#5-base-de-datos-y-modelos-orm) +6. [API REST](#6-api-rest) +7. [Capa LLM](#7-capa-llm) +8. [Pipeline de generación de documentos](#8-pipeline-de-generación-de-documentos) +9. [Sistema de citas académicas](#9-sistema-de-citas-académicas) +10. [Autenticación y sesiones](#10-autenticación-y-sesiones) +11. [Planes, créditos y facturación](#11-planes-créditos-y-facturación) +12. [Almacenamiento (DigitalOcean Spaces)](#12-almacenamiento-digitalocean-spaces) +13. [Frontend](#13-frontend) +14. [Comandos de desarrollo](#14-comandos-de-desarrollo) +15. [Testing](#15-testing) +16. [Despliegue en producción](#16-despliegue-en-producción) + +--- + +## 1. Stack técnico + +### Backend +| Componente | Tecnología | +|---|---| +| Framework | FastAPI 0.115 | +| Runtime | Python 3.11 | +| Base de datos | PostgreSQL 16 (async) | +| ORM | SQLAlchemy 2.0 + asyncpg | +| Migraciones | Alembic | +| Generación .docx | python-docx | +| Autenticación | JWT (python-jose) + bcrypt + Google OAuth | +| Storage | DigitalOcean Spaces (S3-compatible, boto3) | +| Pagos | Stripe (suscripciones + créditos) | +| Cache / Rate limit | Redis 7 | +| Logging | structlog (JSON en prod, consola en dev) | +| Validación | Pydantic v2 + pydantic-settings | +| Linting | ruff + mypy | +| Testing | pytest + pytest-asyncio | + +### LLM / Proveedores externos +| Proveedor | Uso | +|---|---| +| OpenAI | Generación de estructura, contenido y extracción conversacional (activo) | +| DeepSeek | Proveedor alternativo por plan/tarea (configurable) | +| Anthropic | Proveedor Claude (implementado, no activo en MVP) | +| Semantic Scholar | Citas académicas (fuente primaria) | +| Crossref | Metadatos DOI (fuente secundaria) | +| Brave Search | Fallback de citas (búsqueda web) | + +### Frontend +| Componente | Tecnología | +|---|---| +| Framework | Next.js 15 (App Router) | +| UI Library | React 19 | +| Tipado | TypeScript 5 | +| Estilos | Tailwind CSS + Radix UI | +| Animaciones | Framer Motion | +| HTTP | Axios + interceptor global de 401 | +| Forms | React Hook Form + Zod | + +### Infraestructura +| Componente | Tecnología | +|---|---| +| Contenedores | Docker multi-stage (dev y prod) | +| Orquestación | Docker Compose | +| Servidor | DigitalOcean VPS (2GB RAM) | +| Archivos | DigitalOcean Spaces | +| CI/CD | Manual (sin GitHub Actions) | + +--- + +## 2. Arquitectura general + +``` +┌─────────────────────────────────────────────────────┐ +│ FRONTEND │ +│ Next.js 15 · App Router · React 19 · TS │ +│ Conversación ─► Extracción ─► Generación ─► .docx │ +│ Planes / Créditos ─► Stripe Checkout │ +└────────────────────────┬────────────────────────────┘ + │ HTTPS / Cookie HttpOnly + │ NEXT_PUBLIC_API_URL +┌────────────────────────▼────────────────────────────┐ +│ BACKEND (API) │ +│ FastAPI · Python 3.11 · async │ +│ │ +│ /auth ──► AuthService ──► JWT / Google OAuth │ +│ /documents ──► ConversationExtractor ──► LLM │ +│ └──► DocumentGenerationPipeline │ +│ ├─ StructureStep (LLM) │ +│ ├─ ContentStep (LLM, 3 fases) │ +│ ├─ CitationStep (cascade) │ +│ └─ AssemblyStep (python-docx) │ +│ /payments ──► Stripe (suscripciones + créditos) │ +│ /admin ──► SubscriptionService / UsageLogs │ +└─────┬──────────┬────────────┬──────────┬────────────┘ + │ │ │ │ + PostgreSQL DigitalOcean OpenAI / Redis + (async) Spaces (.docx) DeepSeek (rate limit) + Anthropic +``` + +--- + +## 3. Estructura de directorios + +``` +studymation/ +├── backend/ +│ ├── app/ +│ │ ├── api/v1/ +│ │ │ ├── auth.py # Rutas /auth +│ │ │ ├── documents.py # Rutas /conversations y /documents +│ │ │ ├── payments.py # Rutas /payments (Stripe checkout + webhook) +│ │ │ └── admin.py # Rutas /admin +│ │ ├── core/ +│ │ │ ├── attachments/ # Parsers: PDF, DOCX, MD, TXT +│ │ │ ├── citations/ # Pipeline de citas (cascade + caché) +│ │ │ ├── conversation/ # ConversationExtractor +│ │ │ ├── document/ +│ │ │ │ └── pipeline/ # 4 steps de generación +│ │ │ ├── llm/ # Providers (OpenAI, DeepSeek, Anthropic) + factory +│ │ │ └── storage/ # S3Provider + TTL manager +│ │ ├── middleware/ +│ │ │ ├── auth.py # get_current_user / get_current_admin +│ │ │ ├── plan_guard.py # require_plan() dependency +│ │ │ └── rate_limit.py # Rate limiting Redis/memory (por plan e IP) +│ │ ├── models/ # ORM SQLAlchemy +│ │ │ ├── user.py +│ │ │ ├── subscription.py +│ │ │ ├── document.py +│ │ │ ├── conversation.py +│ │ │ ├── citation_cache.py +│ │ │ ├── usage_log.py +│ │ │ ├── credit_package.py # Paquetes de créditos disponibles +│ │ │ └── credit_transaction.py # Ledger inmutable de créditos +│ │ ├── schemas/ # Pydantic request/response schemas +│ │ ├── services/ +│ │ │ ├── auth_service.py +│ │ │ ├── document_service.py +│ │ │ ├── subscription_service.py +│ │ │ └── credit_service.py # Balance + transacciones atómicas +│ │ ├── utils/ +│ │ │ ├── exceptions.py # StudymationError hierarchy +│ │ │ └── logger.py # structlog setup +│ │ ├── config.py # Settings (pydantic-settings) incl. Stripe + FX +│ │ ├── database.py # AsyncEngine + session factory +│ │ └── main.py # Entry point FastAPI +│ ├── alembic/versions/ # Migraciones versionadas +│ ├── schools/ # Plantillas institucionales (CETI, etc.) +│ │ └── / +│ │ └── school.json # Metadata: logo, colores, formato portada +│ ├── tests/ +│ │ ├── unit/ +│ │ ├── integration/ +│ │ └── e2e/ +│ ├── pyproject.toml +│ ├── Dockerfile +│ └── .env.example +│ +├── frontend/ +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── (app)/dashboard/ # Rutas autenticadas +│ │ │ │ ├── generar/ # Flujo conversacional principal +│ │ │ │ ├── documentos/ # Historial de documentos +│ │ │ │ ├── creditos/ # Marketplace de créditos (Stripe) +│ │ │ │ └── cuenta/ # Perfil + gestión de suscripción +│ │ │ ├── (auth)/ # login/, registro/ +│ │ │ ├── admin/ # Panel admin +│ │ │ └── auth/google/callback/ +│ │ ├── components/ +│ │ │ ├── admin/ +│ │ │ ├── conversation/ # Burbujas, progreso de extracción +│ │ │ ├── document/ # Modal disclaimer, pantalla generando +│ │ │ └── ui/ # Toast, primitivos +│ │ ├── hooks/ +│ │ │ ├── useAuth.ts +│ │ │ ├── usePlan.ts +│ │ │ └── useCredits.ts # Balance de créditos +│ │ └── lib/ +│ │ ├── api.ts # Axios instance + namespaced clients +│ │ ├── plans.ts # Definición de planes y precios +│ │ └── utils.ts +│ ├── package.json +│ ├── next.config.ts +│ ├── tailwind.config.ts +│ └── Dockerfile +│ +├── docker-compose.yml # Entorno de desarrollo +├── docker-compose.prod.yml # Entorno de producción +├── Makefile # +60 comandos +└── .env # Variables globales (gitignored) +``` + +--- + +## 4. Configuración y variables de entorno + +Toda configuración del backend pasa por `app/config.py` usando `pydantic-settings`. **Nunca** leer `os.environ` directamente en el código; siempre importar `settings`. En tests, llamar `get_settings.cache_clear()` antes de sobreescribir variables. + +### Variables del backend (`.env`) + +```bash +# ── Aplicación ──────────────────────────────────────────── +APP_ENV=development # development | staging | production +DEBUG=true +ALLOWED_ORIGINS=http://localhost:3000 +FRONTEND_URL=http://localhost:3000 + +# ── LLM (config por plan + tarea) ───────────────────────── +# Formato nuevo (tiene precedencia sobre legacy) +LLM_{PLAN}_{TASK}_PROVIDER=openai # openai | deepseek | anthropic +LLM_{PLAN}_{TASK}_MODEL=gpt-4.1-mini + +# Planes: free | pro | max +# Tareas: structure | content | conversation | citations + +# Fallback legacy (si no hay config por tarea) +LLM_PROVIDER=openai +LLM_MODEL_FREE=gpt-4.1-mini +LLM_MODEL_PRO=gpt-4.1-mini +LLM_MODEL_MAX=gpt-4.1-mini + +# ── API Keys ────────────────────────────────────────────── +OPENAI_API_KEY=sk-... +DEEPSEEK_API_KEY=... +ANTHROPIC_API_KEY=... +BRAVE_SEARCH_API_KEY=... + +# ── Base de datos ───────────────────────────────────────── +DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/studymation +DATABASE_URL_TEST=postgresql+asyncpg://user:pass@db_test:5432/studymation_test + +# ── Redis ───────────────────────────────────────────────── +REDIS_URL=redis://redis:6379/0 +RATE_LIMIT_BACKEND=memory # memory (dev) | redis (prod, multi-worker) + +# ── Storage (DigitalOcean Spaces) ───────────────────────── +STORAGE_ENDPOINT=https://nyc3.digitaloceanspaces.com +STORAGE_BUCKET=studymation-docs +STORAGE_ACCESS_KEY=... +STORAGE_SECRET_KEY=... +FREE_DOCUMENT_TTL_HOURS=72 + +# ── JWT ─────────────────────────────────────────────────── +JWT_SECRET_KEY=... # openssl rand -hex 32 +JWT_EXPIRE_DAYS=7 + +# ── Google OAuth ────────────────────────────────────────── +GOOGLE_CLIENT_ID=... +GOOGLE_CLIENT_SECRET=... + +# ── Stripe ──────────────────────────────────────────────── +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... + +# ── Billing / Créditos ──────────────────────────────────── +USD_TO_MXN=17.44 # Actualizar periódicamente +USD_TO_MXN_LAST_UPDATED=2026-05-08 # Si >7 días, Settings.is_fx_stale() = true +PROFIT_MARGIN_PCT=300 # Margen sobre costo LLM (300 = 3x) +MAX_NEGATIVE_BALANCE=-20.0 # Balance mínimo permitido en créditos + +# ── Crossref (buenas prácticas de la API) ───────────────── +CROSSREF_MAILTO=tu@studymation.online +``` + +### Variables del frontend (`.env.local`) + +```bash +NEXT_PUBLIC_API_URL=http://localhost:8000 +# En producción: https://api.studymation.online +``` + +### Resolución de modelo LLM + +`Settings.get_task_provider_model(plan, task)` resuelve en este orden: + +1. `LLM_{PLAN}_{TASK}_PROVIDER` + `LLM_{PLAN}_{TASK}_MODEL` (específico por plan y tarea) +2. `LLM_PROVIDER` + `LLM_MODEL_{PLAN}` (legacy, fallback) + +--- + +## 5. Base de datos y modelos ORM + +PostgreSQL 16 con SQLAlchemy 2.0 async (asyncpg). Todas las conexiones son asíncronas. El `pool_size` es 5 con `max_overflow` 10. + +### Modelos + +#### `users` +``` +id UUID PK +email TEXT UNIQUE NOT NULL (indexed) +password_hash TEXT NULL (null si es Google OAuth) +google_id TEXT UNIQUE NULL +full_name TEXT NOT NULL +is_active BOOL DEFAULT true +is_admin BOOL DEFAULT false +created_at TIMESTAMPTZ +updated_at TIMESTAMPTZ +``` + +#### `subscriptions` +``` +id UUID PK +user_id UUID FK → users (indexed) +plan ENUM(free, pro, max) +status ENUM(active, suspended, cancelled) +activated_by UUID FK → users NULL (admin que activó o Stripe) +activation_note TEXT NULL (referencia de pago) +stripe_session_id TEXT NULL (Stripe Checkout Session ID) +started_at TIMESTAMPTZ +expires_at TIMESTAMPTZ NULL (null = sin expiración) +``` + +#### `documents` +``` +id UUID PK +user_id UUID FK → users (indexed) +title TEXT NOT NULL +document_type TEXT NOT NULL +school_id TEXT NOT NULL +page_count INT +has_cover_page BOOL +storage_key TEXT NULL (ruta en Spaces) +storage_expires TEXT NULL (TTL para plan Free) +generation_status ENUM(pending, generating, completed, failed) +llm_model_used TEXT (auditoría de costos) +citations_count INT +metadata_ JSONB +error_detail TEXT NULL +created_at TIMESTAMPTZ +updated_at TIMESTAMPTZ +``` + +#### `conversations` +``` +id UUID PK +user_id UUID FK → users (indexed) +document_id UUID FK → documents NULL +status ENUM(active, completed, abandoned) +extracted_data JSONB (campos acumulados por turno) +messages JSONB[] (historial completo de mensajes) +created_at TIMESTAMPTZ +updated_at TIMESTAMPTZ +``` + +#### `citations_cache` +``` +id UUID PK +query_hash TEXT UNIQUE (SHA256 de la query normalizada) +result JSONB (cita verificada o found=false) +provider TEXT (semantic_scholar | crossref | brave) +created_at TIMESTAMPTZ (TTL 30 días) +``` + +#### `usage_logs` +``` +id UUID PK +user_id UUID FK → users +document_id UUID FK → documents NULL +action TEXT (generate | download) +model TEXT +tokens INT +cost_usd NUMERIC(10,6) +created_at TIMESTAMPTZ +``` + +#### `credit_packages` +``` +id UUID PK +code TEXT UNIQUE (ej. "starter", "professional", "enterprise") +name TEXT +credits NUMERIC (cantidad de créditos) +price_usd NUMERIC(10,4) +stripe_price_id TEXT NULL +active BOOL DEFAULT true +created_at TIMESTAMPTZ +``` + +#### `credit_transactions` +``` +id UUID PK +user_id UUID FK → users (indexed) +type ENUM(purchase, consume, refund, bonus) +amount NUMERIC (positivo = ingreso, negativo = consumo) +reference_id TEXT UNIQUE NULL (idempotency key — ej. stripe_session_id) +description TEXT NULL +document_id UUID FK → documents NULL +created_at TIMESTAMPTZ +``` + +### Migraciones + +```bash +make migrate # Aplicar pendientes (alembic upgrade head) +make migrate-create msg="mensaje" # Auto-generar desde modelos ORM +make shell # bash en contenedor para correr alembic manual +``` + +--- + +## 6. API REST + +Base path: `/api/v1`. Todas las rutas de documentos y admin requieren autenticación JWT. + +### Auth (`/api/v1/auth`) + +| Método | Ruta | Descripción | +|---|---|---| +| POST | `/auth/register` | Registro con email + password. Auto-activa plan Free. | +| POST | `/auth/login` | Login. Setea cookie HttpOnly. | +| POST | `/auth/logout` | Limpia cookie HttpOnly. | +| GET | `/auth/google` | Redirige a Google OAuth. | +| GET | `/auth/google/callback` | Callback OAuth. Crea usuario si es nuevo. | +| GET | `/auth/me` | Datos del usuario autenticado. | +| GET | `/auth/me/subscription` | Plan activo: `{ plan, is_trial, expires_at }`. | + +### Documentos (`/api/v1`) + +| Método | Ruta | Descripción | +|---|---|---| +| POST | `/conversations` | Inicia nueva conversación vacía. | +| POST | `/conversations/{id}/turn` | Envía turno: `{ message, files? }`. Retorna pregunta siguiente. | +| POST | `/conversations/{id}/generate` | Dispara el pipeline de generación. | +| GET | `/documents` | Historial de documentos del usuario. | +| GET | `/documents/{id}/citation-audit` | Razones de aceptación/rechazo de citas por sección. | +| GET | `/documents/{id}` | Detalle de un documento del usuario. | +| GET | `/documents/{id}/status` | Estado de generación del documento (`pending`, `processing`, `completed`, `failed`). | +| GET | `/documents/{id}/download` | Genera signed URL de Spaces + registra UsageLog. | +| POST | `/conversations/{id}/direct-submit` | Envía todos los campos de la conversación de una vez (sin turnos). | +| POST | `/documents/{id}/disclaimer-accepted` | Confirma que el usuario revisó el documento. | +| GET | `/schools` | Lista de plantillas institucionales disponibles. | + +### Pagos (`/api/v1/payments`) — requiere autenticación + +| Método | Ruta | Descripción | +|---|---|---| +| POST | `/payments/create-checkout-session` | Stripe Checkout para compra de paquete de créditos. | +| POST | `/payments/create-plan-checkout` | Stripe Checkout para suscripción Pro o Max. | +| POST | `/payments/stripe/webhook` | Webhook Stripe (firma validada). Activa plan o acredita créditos. | + +### Admin (`/api/v1/admin`) — requiere `is_admin=true` + +| Método | Ruta | Descripción | +|---|---|---| +| GET | `/admin/users` | Lista todos los usuarios. | +| GET | `/admin/users/{id}` | Detalle de un usuario. | +| PATCH | `/admin/users/{id}` | Modificar `is_active` o `is_admin`. | +| POST | `/admin/subscriptions` | Activar plan manualmente. | +| GET | `/admin/subscriptions/{id}` | Suscripción activa de un usuario. | +| GET | `/admin/stats/costs` | Estadísticas de tokens y costo USD total y por usuario/modelo. | +| GET | `/admin/users/{id}/costs` | Desglose de costos por documento de un usuario. | +| POST | `/admin/maintenance/cleanup` | Elimina documentos Free con TTL expirado de Spaces. | + +### Respuestas de error + +Todos los errores del dominio heredan de `StudymationError(detail, code)`. El `code` es lo que el frontend usa programáticamente. El handler global mapea `code` a status HTTP. + +```python +# Autenticación +AuthenticationError → 401 +TokenExpiredError → 401 +InsufficientPermissionsError → 403 + +# Usuarios +UserNotFoundError → 404 +UserAlreadyExistsError → 409 + +# Planes / Créditos +PlanLimitExceededError → 429 +FeatureNotAvailableError → 403 +InsufficientCreditsError → 402 +InvalidUploadError → 413 + +# Documentos +ConversationNotReadyError → 400 +DocumentNotFoundError → 404 +DocumentExpiredError → 410 +DocumentGenerationError → 500 + +# Citas +CitationNotVerifiedError → 500 + +# Instituciones +SchoolNotFoundError → 404 +``` + +--- + +## 7. Capa LLM + +### Providers (`app/core/llm/`) + +- `BaseLLMProvider` — interfaz común (método `complete(messages, model, ...)`) +- `OpenAIProvider` — wrapper async para OpenAI SDK +- `DeepSeekProvider` — compatible con API OpenAI (base URL diferente) +- `AnthropicProvider` — wrapper para SDK Anthropic (Claude Opus/Sonnet/Haiku); implementado pero no activo en MVP +- `factory.py` — `get_llm_for_plan(plan, task)` devuelve instancia del provider + nombre del modelo + +### Configuración por plan y tarea + +El sistema soporta configuración granular vía env vars. Ejemplo para que Free use DeepSeek en generación de contenido pero OpenAI en conversación: + +```bash +LLM_FREE_CONTENT_GENERATION_PROVIDER=deepseek +LLM_FREE_CONTENT_GENERATION_MODEL=deepseek-chat +LLM_FREE_CONVERSATION_EXTRACTION_PROVIDER=openai +LLM_FREE_CONVERSATION_EXTRACTION_MODEL=gpt-4.1-mini +``` + +Si no se define una combinación plan+tarea, cae al legacy `LLM_PROVIDER` + `LLM_MODEL_{PLAN}`. + +### ContentStep — arquitectura de 3 fases + +`ContentStep` implementa un pipeline de calidad en 3 fases para evitar repetición y asegurar coherencia: + +| Fase | Descripción | Modelo | +|---|---|---| +| 0 — Planificación global | Define tesis del documento + 3-4 afirmaciones por sección | Nano (mini) | +| 1 — Desarrollo de secciones | Genera cada sección con contexto enriquecido (afirmaciones, temas a evitar, secciones adyacentes) | Configurado por plan | +| 2 — Intro y conclusión | Generados después del desarrollo, usando resúmenes reales de cada sección | Configurado por plan | + +Validaciones adicionales: +- Longitud mínima por tipo de sección (Intro: 100 chars, Secciones: 150 chars, Conclusión: 100 chars) +- Detección de solapamiento semántico entre secciones adyacentes (Jaccard ≥ 0.40 → autocorrección, máx. 1 llamada extra) + +--- + +## 8. Pipeline de generación de documentos + +`DocumentGenerationPipeline` en `app/core/document/pipeline/` ejecuta 4 pasos de forma secuencial: + +### Paso 1 — StructureStep +- Llama al LLM con el `extracted_data` de la conversación +- Devuelve un outline JSON con intención, keywords, conceptos y queries de cita por sección +- Usa plantilla institucional de `schools//school.json` para adaptar formato y portada + +### Paso 2 — ContentStep (3 fases) +- Fase 0: planificación global con modelo nano (+1 llamada LLM por documento) +- Fase 1: secciones en paralelo con contexto enriquecido +- Fase 2: intro y conclusión en paralelo, generados a partir de los resúmenes reales +- Respeta el límite de páginas del plan del usuario + +### Paso 3 — CitationStep +- Por cada sección con intención de cita, busca candidatos académicos y los rankea por relevancia +- Usa `CitationPipeline` (ver sección 9) +- Inserta citas en texto de forma determinista; la bibliografía solo incluye fuentes usadas +- Si no encuentra cita útil, declara `found=False` explícitamente (nunca inventa ni infla referencias) + +### Paso 4 — AssemblyStep +- Recibe estructura + contenido + citas +- Construye el `.docx` con python-docx +- Aplica estilos institucionales: portada (logo, colores del `school.json`), cuerpo con markdown-lite, tablas, listas, blockquotes +- Intenta insertar imágenes reales vía Brave con fallback a diagrama Mermaid si no se encuentra imagen válida + +### Orquestación en `document_service.py` + +``` +1. Verificar límites del plan (SubscriptionService) +2. Crear registro Document (status: PENDING → GENERATING) +3. Ejecutar DocumentGenerationPipeline +4. Subir .docx a DigitalOcean Spaces +5. Actualizar Document (status: COMPLETED, storage_key, citations_count) +6. Registrar UsageLog (tokens, cost_usd) +``` + +--- + +## 9. Sistema de citas académicas + +`CitationPipeline` en `app/core/citations/` implementa búsqueda multi-proveedor, +deduplicación, scoring semántico y auditoría con caché en PostgreSQL. + +### Flujo de ranking + +``` +Contexto de sección + │ + ▼ +Queries propias + topic + conceptos clave + │ + ▼ +Cache + Semantic Scholar + Crossref + Brave + │ + ▼ +Normalización + deduplicación por DOI/título + │ + ▼ +CitationRanker + │ score >= umbral + ▼ +Cita insertada en texto + referencia APA 7 + │ score bajo + ▼ +found=False con razones de rechazo +``` + +### Caché + +- Clave: SHA256 de la query normalizada +- TTL configurable con `CITATION_CACHE_TTL_DAYS` +- Tabla: `citations_cache` +- Los resultados cacheados se revalidan contra la sección actual antes de aceptarse + +### Validación y formato + +- `CitationRelevanceValidator` separa DOI válido de cita académica útil +- `CitationRanker` usa relevancia título/query/sección/contenido, metadatos, proveedor y penalizaciones +- Rechaza coincidencias superficiales, títulos genéricos, duplicados, fuentes sin metadatos y resultados fuera de dominio +- `CitationFormatter` formatea la salida en APA 7 +- `GET /api/v1/documents/{id}/citation-audit` expone razones de aceptación/rechazo + +--- + +## 10. Autenticación y sesiones + +### Flujo email + password + +``` +POST /auth/register + → hash bcrypt + → crear User + → auto-activar plan Free (trial 3 meses) + → crear JWT (exp: JWT_EXPIRE_DAYS) + → Set-Cookie: studymation_token (HttpOnly, Secure en prod) + +POST /auth/login + → verificar bcrypt + → crear JWT + → Set-Cookie +``` + +### Flujo Google OAuth + +``` +GET /auth/google + → guardar state en cookie temporal (10 min, SameSite=lax) + → redirect a accounts.google.com + +GET /auth/google/callback?code=...&state=... + → validar state + → intercambiar code por tokens Google + → si usuario nuevo: crear con google_id + → si existe: actualizar google_id + → crear JWT → Set-Cookie +``` + +### Verificación de sesión + +Todas las rutas protegidas usan la dependencia `get_current_user()`: + +``` +Prioridad de token: + 1. Header Authorization: Bearer + 2. Cookie studymation_token + 3. 401 si ninguno presente o expirado +``` + +### Middleware de plan + +`require_plan(["pro", "max"])` es una dependency factory que verifica el plan activo del usuario antes de procesar la request. Devuelve `FeatureNotAvailableError (403)` si el plan no es suficiente. + +--- + +## 11. Planes, créditos y facturación + +### Planes de suscripción + +| Plan | Precio MXN | Docs/mes | Docs/semana | Máx páginas | Almacenamiento | +|---|---|---|---|---|---| +| **Free** | $0 | 2 | — | 3 | 72 h TTL | +| **Pro** | $99/mes | — | 15 | 20 | Permanente | +| **Max** | $149/mes | ∞ | ∞ | ∞ | Permanente | + +La lógica de límites está en `app/services/subscription_service.py`. La definición de precios y features para el frontend está en `frontend/src/lib/plans.ts`. + +### Activación de planes vía Stripe + +Los planes Pro y Max se activan mediante Stripe Checkout: + +``` +Frontend → POST /payments/create-plan-checkout + → Stripe crea sesión de pago + → Usuario completa pago en Stripe + → Stripe → POST /payments/stripe/webhook (checkout.session.completed) + → Backend activa suscripción con stripe_session_id como referencia +``` + +También pueden activarse manualmente desde el panel admin (ej. para pagos SPEI o bonificaciones). + +### Sistema de créditos + +1 crédito = 1 MXN. Los créditos cubren el costo de LLM con un margen configurado (`PROFIT_MARGIN_PCT`). + +**Paquetes disponibles** (configurables en tabla `credit_packages`): +- Starter: 100 créditos +- Professional: 500 créditos +- Enterprise: 1000+ créditos + +**Flujo de compra:** +``` +Frontend → POST /payments/create-checkout-session { package_code } + → Stripe crea sesión + → Usuario paga + → Stripe webhook → CreditService.add_credits() con stripe_session_id como reference_id +``` + +**Garantías del sistema de créditos:** +- Transacciones atómicas: `SELECT FOR UPDATE` en tabla de créditos +- Idempotencia: `reference_id` único evita acreditaciones duplicadas +- Balance negativo permitido hasta `MAX_NEGATIVE_BALANCE` (default: -20.0) +- El backend es source of truth — el frontend no puede alterar precios ni cantidades + +**Tipo de cambio:** `USD_TO_MXN` en `.env`. Si `Settings.is_fx_stale()` detecta que la fecha `USD_TO_MXN_LAST_UPDATED` tiene más de 7 días, el endpoint de checkout emite advertencia en logs. + +### Rate limiting + +| Contexto | Límite | +|---|---| +| Free | 10 requests/min | +| Pro | 30 requests/min | +| Max | 60 requests/min | +| Auth (login/register) | 10 intentos/min por IP | + +Configurar `RATE_LIMIT_BACKEND=redis` en producción para que el límite sea compartido entre workers. + +--- + +## 12. Almacenamiento (DigitalOcean Spaces) + +`app/core/storage/s3_provider.py` usa boto3 con la API S3-compatible de Spaces. + +### Operaciones principales + +| Operación | Descripción | +|---|---| +| `upload_document(bytes, key)` | Sube `.docx` al bucket | +| `generate_presigned_url(key, expires_in)` | URL de descarga firmada (tiempo limitado) | +| `delete_document(key)` | Elimina archivo del bucket | + +### TTL de documentos Free + +Los documentos del plan Free tienen una expiración de `FREE_DOCUMENT_TTL_HOURS` (default 72 h). La limpieza se ejecuta manualmente: + +```bash +make cleanup-docs +# o +POST /api/v1/admin/maintenance/cleanup +``` + +--- + +## 13. Frontend + +### Cliente API (`src/lib/api.ts`) + +Axios instance con `withCredentials: true` para enviar automáticamente la cookie HttpOnly. Interceptor global que redirige a `/login` en respuestas 401. + +```typescript +// Clients disponibles +authApi // register, login, logout, me, mySubscription +documentsApi // startConversation, sendTurn, generate, listDocuments, download, listSchools +paymentsApi // createCheckoutSession, createPlanCheckout +adminApi // listUsers, getUser, updateUser, activatePlan, getCostStats, ... +``` + +Nunca usar `fetch` directo en componentes; siempre pasar por estos clients. + +### Hooks principales + +**`useAuth`** — verifica sesión en mount con `GET /auth/me`. Expone `{ user, loading, isAuthenticated, logout, refetch }`. + +**`usePlan`** — llama `GET /auth/me/subscription` cuando `isAuthenticated=true`. Expone `{ plan, loading, is_trial, expires_at }`. + +**`useCredits`** — balance de créditos del usuario. Expone `{ balance, loading, refetch }`. + +### Rutas + +``` +/ Landing page +/login Login +/registro Registro +/auth/google/callback Callback OAuth (fuera del grupo auth) + +(autenticadas — redirigen a /login si no hay sesión) +/dashboard Home del usuario +/dashboard/generar Flujo conversacional principal +/dashboard/documentos Historial de documentos +/dashboard/creditos Marketplace de créditos (Stripe Checkout) +/dashboard/cuenta Perfil + gestión de suscripción (upgrade a Pro/Max) + +(admin — requieren is_admin=true) +/admin Panel principal +/admin/usuarios Gestión de usuarios +/admin/suscripciones Activar planes +/admin/costos Estadísticas LLM (tokens, USD) +``` + +### Flujo de generación (`/dashboard/generar`) + +``` +1. startConversation() → conversation_id +2. Loop de turnos: + sendTurn({ message, files? }) → { assistant_message, extracted_data, + ready_to_generate, missing_fields } +3. ready_to_generate = true: + generate() → Document (status: generating) + → mostrar GeneratingScreen +4. status: completed: + → mostrar DisclaimerModal +5. Usuario confirma: + download() → signed URL + → descarga automática del .docx +``` + +Los 8 campos requeridos para `ready_to_generate`: +`topic`, `document_type`, `subject`, `student_name`, `student_id`, `grade`, `group`, `teacher` + +--- + +## 14. Comandos de desarrollo + +Todos los comandos se ejecutan desde la raíz del repo con `make`. El Makefile usa `docker-compose.prod.yml` para operaciones de backend por defecto. + +### Servicios + +```bash +make up-detached # Levantar servicios en background +make up # Levantar con logs en foreground +make down # Apagar servicios +make restart # Reiniciar +make status # Ver estado de contenedores +make logs # Tail de todos los logs +make logs-api # Solo logs del API +make stats # CPU y memoria de contenedores +make health # curl /health +``` + +### Base de datos + +```bash +make migrate # alembic upgrade head +make migrate-create msg="mensaje" # Auto-generar migración +make db-reset # DROP + CREATE dev DB (destructivo) +make psql # psql interactivo en contenedor +make shell # bash dentro del contenedor API +``` + +### Calidad de código + +```bash +make lint # ruff check app/ tests/ +make format # ruff format app/ tests/ +make typecheck # mypy app/ +make check # lint + typecheck +``` + +### Frontend (local) + +```bash +make fe-dev # Next.js dev server con hot-reload (npm run dev) +make fe-build # Build de producción +make fe-lint # ESLint +make dev # Backend (Docker) + Frontend (local) simultáneamente +``` + +### Admin + +```bash +make create-admin email=user@ejemplo.com # Promover usuario a admin +make cleanup-docs # Limpiar docs Free expirados +``` + +--- + +## 15. Testing + +Los tests corren **dentro del contenedor API** (`docker compose exec api pytest ...`). + +### Configuración + +- `conftest.py` crea la BD `studymation_test` al inicio de la sesión +- Cada test obtiene una transacción que hace rollback automático al terminar (sin limpieza manual) +- BD de test separada en `db_test` (puerto 5433) + +### Markers + +```python +@pytest.mark.slow # Llama APIs externas reales (Semantic Scholar, Brave, etc.) +@pytest.mark.unit # Sin BD, sin I/O externa +@pytest.mark.integration # Requiere BD de test +``` + +### Comandos + +```bash +make test # Todos excepto slow (-m "not slow") +make test-unit # Solo unit +make test-integration # Solo integration +make test-slow # Solo tests que llaman APIs externas +make test-coverage # Con reporte HTML en htmlcov/ +make test-file f=tests/unit/test_auth.py # Un archivo específico +``` + +--- + +## 16. Despliegue en producción + +### Configuración Docker Compose de producción + +`docker-compose.prod.yml` difiere del de desarrollo en: +- Sin pgAdmin, sin `db_test`, sin hot-reload +- Límites de memoria: API 800MB, DB 512MB, Frontend 512MB, Redis 64MB +- Puerto 5432 no expuesto (solo red interna Docker) +- `restart: always` en todos los servicios +- API con 2 workers uvicorn +- Redis con `RATE_LIMIT_BACKEND=redis` para seguridad multi-worker + +### Comandos de producción + +```bash +make prod-build # Construir imágenes de producción +make prod-up # Levantar servicios prod +make prod-logs # Tail logs prod +make prod-migrate # alembic upgrade head en prod +make prod-create-admin email=user@mail.com # Crear admin en prod +make prod-health # Verificar /health en prod +``` + +### Checklist de primer despliegue + +1. Copiar `.env.example` → `.env` y completar todas las variables (incluyendo Stripe keys) +2. Copiar `frontend/.env.local.example` → `frontend/.env.local` con la URL de API de prod +3. Configurar webhook en Stripe Dashboard apuntando a `https://api.studymation.online/api/v1/payments/stripe/webhook` +4. `make prod-build` +5. `make prod-up` +6. `make prod-migrate` +7. `make prod-create-admin email=...` +8. Verificar con `make prod-health` + +### Variables críticas para producción + +```bash +APP_ENV=production +DEBUG=false +# AUTH_COOKIE_SECURE se activa automáticamente cuando APP_ENV=production — no configurar explícitamente. +AUTH_COOKIE_SAMESITE=lax +ALLOWED_ORIGINS=https://studymation.online +NEXT_PUBLIC_API_URL=https://api.studymation.online +RATE_LIMIT_BACKEND=redis # Multi-worker safe +STRIPE_SECRET_KEY=sk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... +POSTGRES_PASSWORD=... # Debe coincidir con la contraseña en DATABASE_URL +```