尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Pedidai es una plataforma web de gestión empresarial B2B diseñada para que las pymes centralicen y optimicen sus operaciones de compra y gestión de proveedores.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Descubre cuánto pagas de más a tus proveedores.
PedidAI es una aplicación web para bares y restaurantes (de 1 a 5 locales) que hacen sus propias compras:
Versión: 2.4.0 · Última actualización: 17 de octubre de 2026
Navegador (Angular)
│ https://pedidai.es
▼
┌─────────────────────┐
│ nginx │
└──┬───────┬───────┬──┘
páginas web │ /api │ /ai │
▼ ▼ ▼
/var/www/pedidai.es Spring Boot Asistente (Docker, Node.js)
(ficheros de Angular) :8085 :3201 ──► Mistral AI
│ ▲ │
│ └───────────────┘ REST con el token
│ del propio usuario
▼
MySQL
Spring Boot ── webhook (registro nuevo) ──► n8n (Docker, :5678) ──► SMTP ──► email al equipo
n8n ── GET /api/internal/daily-summary (8:00) ──► Spring Boot
| Componente | Responsabilidad |
|---|---|
pedidai-app (Angular) | Interfaz web. Se sirve como ficheros estáticos y consume la API; el interceptor añade el JWT y el idioma (Accept-Language). |
| nginx | HTTPS y proxy inverso: / → Angular, /api → Spring Boot, /ai/ → asistente, /n8n/ → n8n. |
pedidai-api (Spring Boot) | Lógica de negocio, autenticación JWT, aislamiento por empresa, emails (SMTP), PDF y lectura de albaranes (Tesseract + Mistral AI). Único acceso a MySQL. |
| MySQL | Persistencia (pedidai_db). |
Asistente de pedidos (orderflow) | Chat de pedidos: Express + function calling de Mistral AI. Cada herramienta llama a la API con el token del usuario, por lo que tiene sus mismos permisos. Crea pedidos en estado PENDING; el envío siempre lo confirma el usuario desde la web. |
n8n (orderflow) | Emails informativos al equipo: aviso de cada registro nuevo (webhook que llama la API tras el alta) y resumen diario a las 8:00 con registros, pruebas que acaban, pruebas vencidas, borrados próximos y actividad (lee GET /api/internal/daily-summary). |
| Docker | Ejecuta el asistente y n8n (docker compose en orderflow). |
.xlsx/.xls, CSV o foto): el usuario sube cualquier documento y la IA decide si es un albarán o factura o una tarifa (kind=AUTO); en la revisión se puede cambiar. Se leen igual que los albaranes, pero sus precios se guardan como precio ofrecido (price_history.source = QUOTE, sin cantidad y con valid_until si el documento lo indica; si no, 180 días). Cuentan en la comparativa entre proveedores y en el chat, pero no en las subidas de precio ni en el «pagado de más». Hasta 8 páginas o 3.000 filas; si el texto supera 40.000 caracteres se avisa de que solo se ha leído una parte.GET /api/orders/consumption-analysis), sin IA; el usuario crea el pedido pendiente con un clic. También aparecen en el panel de inicio.SUSPENDED). En empresas en prueba, +14 días de prueba (se suman al final de la prueba actual) y activar el plan de pago; no se permite en clientes de pago ni en la cuenta de la plataforma.TrialNotificationServiceImpl).DataRetentionServiceImpl).environment.prod.ts: adsId, adsSignupLabel).| Capa | Tecnología |
|---|---|
| Frontend | Angular 21 (standalone, signals, sin zone.js), TypeScript 5.9, Tailwind CSS 4, ngx-translate 17, Vitest |
| Backend | Java 25, Spring Boot 3.5 (Web, Data JPA, Security, Validation, Mail), JJWT, OpenPDF, SpringDoc (Swagger) |
| Base de datos | MySQL 8 (InnoDB, utf8mb4); H2 en memoria para los tests |
| IA | Tesseract OCR (castellano + catalán) en el servidor; Mistral AI (empresa europea, servidores en la UE) para estructurar albaranes y para el chat. Configurable: cualquier API compatible con OpenAI |
| Asistente | Node.js 20 + Express 5, en Docker |
| Automatización | n8n 2 en Docker: emails informativos al equipo (webhook + tarea diaria + SMTP) |
| Servidor | VPS Ubuntu, nginx (HTTPS con Let's Encrypt), systemd, ufw |
pedidai/
├── pedidai-app/ # Frontend Angular
├── pedidai-api/ # Backend Spring Boot
└── pedidai-db/ # Esquema SQL, migraciones y documentación de tablas
orderflow/ # Repositorio aparte
├── assistant/ # Asistente de pedidos por chat (Express + Mistral AI + herramientas)
├── n8n-workflows/ # Flujos de n8n: aviso de registro nuevo y resumen diario
└── docker-compose.yml # Levanta el asistente y n8n
pedidai-app/src/app)app.routes.ts # Rutas; cada página se carga bajo demanda (lazy loading)
app.config.ts # HttpClient, router, traducciones
interceptors/ # jwt-interceptor: añade el token y el idioma (Accept-Language)
guards/ # Zona privada, sección Gestión (ADMIN) y panel SUPER_ADMIN
services/ # Llamadas HTTP a la API y al asistente; idioma; analítica
layouts/ # public-layout (web pública) y private-layout (aplicación)
pages/ # Landing, sobre nosotros, contacto, legales, superadmin
components/ # Pantallas de la aplicación: dashboard, prices, invoices,
# ai-chat, orders, suppliers, products, users, reports, company, auth
shared/ # Sidebar, alertas, modales, paginación, aviso de cookies,
# pipe `lines` (una frase por línea en pantallas grandes)
public/i18n/ # es.json y ca.json
Rutas principales:
| Zona | Rutas |
|---|---|
| Pública | /, /login, /register, /verify-email, /recover-password, /reset-password, /sobre-nosotros, /contacto, /privacidad, /cookies, /terminos, /aviso-legal |
| Privada | /dashboard, /prices, /invoices/scan, /chat, /suggestions, /orders, /suppliers, /products, /users, /reports, /company |
| SUPER_ADMIN | /superadmin, /superadmin/companies, /superadmin/users |
Las rutas de la web no pueden empezar por /api, /ai ni /n8n: nginx las envía a la API, al asistente o a n8n (por eso el chat está en /chat).
pedidai-api/src/main/java/com/pedidai/api)controllers/ # Endpoints REST (ver sección 5); Pages acota la paginación
services/impl/ # Lógica de negocio; cada consulta se limita a la empresa del usuario
repositories/ # Acceso a datos (Spring Data JPA)
entities/ # Company, User, Supplier, Product, PriceHistory, Order, OrderItem
dto/ # Objetos de entrada/salida de la API (con validaciones)
security/ # JWT, filtro de autenticación, CurrentUser, límites de uso, política de contraseñas
config/ # Idiomas (MessageSource), traducción de respuestas, Swagger, recursos web
exceptions/ # Errores con clave de traducción y manejador global (sin detalles internos)
resources/i18n/ # messages.properties (castellano) y messages_ca.properties (catalán)
Capas: Petición HTTP → JwtAuthenticationFilter → Controller → Service → Repository → MySQL.
Todas las rutas empiezan por /api y, salvo las marcadas como públicas, necesitan la cabecera Authorization: Bearer <token>. Las respuestas siguen el formato { success, message, data }, con el mensaje en el idioma de la cabecera Accept-Language (es o ca). Documentación interactiva en /swagger-ui.html.
Los listados paginados (page, size, sortBy, sortDir) devuelven como máximo 200 elementos por página; ordenar por un campo que no existe responde 400.
| Recurso | Endpoints |
|---|---|
/auth (públicos) | POST /login, /forgot-password, /reset-password, /verify-email, /resend-verification |
/companies | POST /register (público), GET /, PUT /, GET /my-plan |
/users | GET /me, PATCH /me/language, POST /me/resend-verification, CRUD (GET, POST, PUT /{uuid}, PATCH /{uuid}/status, PATCH /{uuid}/change-password, DELETE /{uuid}), GET /search, GET /filter |
/suppliers | CRUD, GET /search, GET /filter, PATCH /{uuid}/status |
/products | POST /create, GET /{uuid}, PUT /{uuid}, PATCH /deactivate/{uuid}, GET /search, GET /filter, GET /compare-prices, POST /upload/{uuid}, POST /upload-temp |
/invoices | POST /scan (imagen o PDF), POST /confirm |
/prices | GET /overview?days= — comparativa, alertas y estimación de ahorro |
/orders | POST /create, GET / (filtros), GET /{uuid}, PUT /update/{uuid}, POST /{uuid}/send, PATCH /{uuid}/cancel (solo pendientes), PATCH /delete/{uuid}, GET /consumption-analysis |
/reports | GET /dashboard, GET /global, GET /global/pdf |
/superadmin | GET /dashboard, /companies, /companies/{uuid}, /users, /stats/monthly; PATCH /companies/{uuid}/status, , |
Asistente (/ai, servido por orderflow): GET /health, POST /process-order, POST /suggest-orders (sugerencias de pedido). Todos menos /health exigen el token del usuario.
Interno (solo desde el propio servidor, para n8n): GET /api/internal/daily-summary?hours=24.
Tablas: companies, users, suppliers, products, price_history, orders, order_items.
companies ─┬─< users
├─< suppliers ─< products ─< price_history
├─< price_history
└─< orders ─< order_items >─ products
id interno y un uuid público (la API nunca expone los id).companies.trial_ends_at: fin de la prueba (NULL = cliente de pago).users.language: idioma del usuario (es/ca) para emails y PDF.products.canonical_name: nombre genérico para comparar el mismo producto entre proveedores.price_history: cada precio leído de un albarán, de una tarifa (source = QUOTE, con valid_until) o introducido a mano, con su fecha.ddl-auto=none): pedidai-db/pedidai_db_schema.sql y, en orden, las migraciones de pedidai-db/migrations/.Detalle de columnas en pedidai-db/readme.md.
CurrentUser); no se puede leer ni modificar nada de otra empresa.USER (pedidos), ADMIN (además informes, datos de la empresa y usuarios) y SUPER_ADMIN (plataforma). Los permisos se aplican en la API (CurrentUser.requireAdmin()) y la web oculta el menú y protege las rutas (admin.guard). Nadie puede asignarse un rol superior al suyo.POST /api/auth/refresh), así que solo caduca tras 30 días sin abrir la app. Lleva la versión de sesión del usuario (users.token_version): al cambiar o restablecer la contraseña sube y se cierran las sesiones de los demás dispositivos. En cada petición se vuelve a comprobar en la base de datos que el usuario y la empresa siguen activos.PedidAI <[email protected]> a través de Brevo, con DKIM de pedidai.es y DMARC; la recepción en hola@ sigue en Cloudflare Email Routing./api/internal/): solo responden a peticiones locales que no pasan por nginx, y nginx además los bloquea; los webhooks de n8n tampoco son accesibles desde fuera./n8n/) y Swagger (/swagger-ui, /v3/api-docs) piden una contraseña de nginx antes de llegar a la aplicación (/etc/nginx/.htpasswd-pedidai; las credenciales, en /root/pedidai-admin-acceso.txt). La versión de n8n está fijada en el docker-compose.yml de orderflow./etc/nginx/snippets/pedidai-csp.conf): solo scripts propios y de Google (Analytics y Ads); no se aplica a n8n ni a Swagger. Por eso el build no incrusta CSS crítico en () ni usa (ambos meten código en línea). y incluyen y : sin ellos se bloquea el envío de la conversión de Google Ads (lo avisa la Consola de Tag Assistant). incluye para el tutorial de la portada (el reproductor solo se carga al pulsar el botón de play).| Componente | Versión |
|---|---|
| Java JDK | 25 |
| Node.js / npm | 22 / 10 |
| MySQL | 8 (o Docker) |
| Tesseract OCR | 5, con los idiomas spa y cat |
| Docker | Opcional, para el asistente y n8n |
CREATE USER 'pedidai_user'@'localhost' IDENTIFIED BY 'password_seguro';
CREATE DATABASE pedidai_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON pedidai_db.* TO 'pedidai_user'@'localhost';
mysql -u pedidai_user -p pedidai_db < pedidai-db/pedidai_db_schema.sql
mysql -u pedidai_user -p pedidai_db < pedidai-db/migrations/001_lanzamiento.sql
mysql -u pedidai_user -p pedidai_db < pedidai-db/migrations/002_empresa_suspendida.sql
mysql -u pedidai_user -p pedidai_db < pedidai-db/migrations/003_avisos_prueba.sql
mysql -u pedidai_user -p pedidai_db < pedidai-db/migrations/004_tarifas.sql
mysql -u pedidai_user -p pedidai_db < pedidai-db/migrations/005_sesion_larga.sql
Con Docker, en lugar de instalar MySQL (los scripts se cargan igual, con docker exec -i pedidai-mysql-local mysql …):
docker run -d --name pedidai-mysql-local -p 127.0.0.1:3306:3306 \
-e MYSQL_ROOT_PASSWORD=… -e MYSQL_DATABASE=pedidai_db -e MYSQL_USER=pedidai_user -e MYSQL_PASSWORD=… \
-v pedidai-mysql-local:/var/lib/mysql mysql:8.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
Variables de entorno necesarias (nunca las subas al repositorio):
export DB_USER_PEDIDAI=pedidai_user
export DB_PASS_PEDIDAI=password_seguro
export [email protected] # SMTP de Gmail (por defecto)
export MAIL_PASS_PEDIDAI=contraseña_de_aplicación
# En producción el correo sale por Brevo: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, MAIL_FROM y MAIL_FROM_NAME
export JWT_SECRET=un_secreto_largo_y_aleatorio
export AI_API_KEY=tu_clave_de_mistral # opcionales: AI_API_URL, AI_MODEL
# Opcionales: orígenes CORS (por defecto los dominios públicos y localhost:4200)
# export CORS_ALLOWED_ORIGINS=https://pedidai.es,http://localhost:4200
# Aviso a n8n de cada registro nuevo (vacío = desactivado)
export N8N_NEW_COMPANY_WEBHOOK=http://127.0.0.1:5678/webhook/pedidai-nuevo-registro
cd pedidai-api
./mvnw spring-boot:run # http://localhost:8085 (Swagger en /swagger-ui.html)
./mvnw test # tests (usan H2, no necesitan MySQL)
Opciones útiles al arrancar: --app.frontend.url=http://localhost:4200, --app.ocr.tesseract-command=/ruta/a/tesseract y, para depurar, --logging.level.com.pedidai.api=DEBUG --spring.jpa.show-sql=true.
Perfil local (alternativa a las variables de entorno): ./mvnw spring-boot:run -Dspring-boot.run.profiles=local carga pedidai-api/config/application-local.properties, que no se versiona (.gitignore). Ahí van spring.datasource.username/password, app.jwt.secret, ai.api.key, app.frontend.url, app.ocr.tesseract-command y un SMTP inexistente (spring.mail.host=localhost, port=2525, sin autenticación ni STARTTLS) para que los envíos fallen sin más. No debe estar en src/main/resources: entraría en el jar.
Tesseract en Windows: winget install UB-Mannheim.TesseractOCR instala solo el inglés. Los idiomas spa.traineddata y cat.traineddata se descargan de tessdata_fast a una carpeta propia (con eng y osd), que se indica en la variable TESSDATA_PREFIX antes de arrancar la API; app.ocr.tesseract-command=C:/Program Files/Tesseract-OCR/tesseract.exe.
cd pedidai-app
npm install
npx ng serve # http://localhost:4200
npx ng test --watch=false # tests
npx ng build # producción → dist/pedidai-app/browser
src/environments/environment.ts apunta a http://localhost:8085/api. proxy.conf.json redirige /ai al asistente en 127.0.0.1:3201.
cd orderflow/assistant
cp .env.example .env # PEDIDAI_API_URL=http://localhost:8085/api y AI_API_KEY
npm install && npm start # http://127.0.0.1:3201/health
n8n se levanta con docker compose up -d n8n en orderflow; los flujos se importan desde orderflow/n8n-workflows/ y necesitan una credencial SMTP (ver el README de orderflow).
Todo corre en un VPS Ubuntu:
| Pieza | Dónde |
|---|---|
| Web (Angular) | /var/www/pedidai.es, servida por nginx |
| API (Spring Boot) | /opt/apps/pedidai/pedidai-api/target/*.jar, servicio pedidai-api.service (puerto 8085, solo local) |
| Configuración y secretos de la API | /opt/apps/pedidai/.env (permisos 600, cargado con EnvironmentFile=): credenciales, JWT_SECRET, AI_API_KEY, SERVER_ADDRESS=127.0.0.1, CORS_ALLOWED_ORIGINS y N8N_NEW_COMPANY_WEBHOOK; con el pago en línea, también STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_ID y STRIPE_TAX_RATE_ID (ver docs/pago-stripe.md) |
| Imágenes de productos | /opt/apps/pedidai/pedidai-api/img/productes/ |
| Asistente y n8n | /opt/apps/orderflow, con docker compose, red host y escuchando solo en local (asistente en 127.0.0.1:3201, n8n en 127.0.0.1:5678) |
| Base de datos | MySQL local, pedidai_db |
nginx publica / (Angular, con index.html y /i18n/ en no-cache), /api → localhost:8085 (y location = /api/invoices/scan aparte, con proxy_read_timeout 240s: leer una tarifa larga puede tardar hasta 3 minutos), /ai/ → localhost:3201/ y /n8n/ → localhost:5678/. nginx bloquea /api/internal/ y los webhooks de n8n, y añade las cabeceras de seguridad (/etc/nginx/snippets/pedidai-security-headers.conf). El cortafuegos (ufw) solo deja pasar HTTP/HTTPS y SSH.
La API y la web se despliegan desde este PC con scripts/desplegar.sh (Git Bash). En el servidor no hay copia del repositorio: el jar y la web se generan en local.
scripts/desplegar.sh api # tests + jar + copia de seguridad + cambio + comprobación
scripts/desplegar.sh web # tests + build de Angular + copia de seguridad + cambio + comprobación
scripts/desplegar.sh api --simular # solo tests y build, sin tocar el servidor
El script se para si falla un test, comprueba que el jar no lleva application-local.properties, guarda la base de datos y el jar o la web actuales en /root/backups/despliegue-<fecha>/, cambia el jar con el servicio parado y, si la API no responde en 90 s, vuelve sola al jar anterior. La web se copia sin --delete y sin tocar .well-known/.
pedidai-db/migrations/, si las hay (antes de desplegar la API).scripts/desplegar.sh api y/o scripts/desplegar.sh web./opt/apps/orderflow, git fetch && git merge --ff-only origin/main (sin sudo: el repositorio es del usuario ubuntu); después sudo docker compose up -d --build assistant (asistente) o sudo docker compose up -d n8n.https://pedidai.es/api/users/me debe responder 401 sin token y https://pedidai.es/ai/health {"status":"ok"}.Para sustituir el jar conviene parar antes el servicio (systemctl stop, copiar y systemctl start): si se cambia con la API en marcha, el proceso antiguo registra un NoClassDefFoundError al apagarse.
| Comando | Uso |
|---|---|
sudo pedidai-set-ai-key | Guarda la clave de la API de IA (la pide oculta y la valida contra Mistral) |
sudo pedidai-set-smtp | Guarda las credenciales SMTP de Brevo (las valida antes de guardarlas) |
sudo pedidai-borrar-empresa [email protected] | Borra una empresa en prueba y todos sus datos a partir del email de uno de sus usuarios. Muestra lo que va a borrar, pide escribir BORRAR, hace antes una copia de la base de datos y nunca toca la cuenta de la plataforma ni clientes de pago |
Se ejecutan con ssh -t vps … para que puedan pedir datos por teclado.
| Síntoma | Causa probable |
|---|---|
La API no arranca: Access denied for user | Credenciales de MySQL o variables DB_* incorrectas. |
Port 8085 is already in use | Ya hay otra instancia de la API en marcha. |
| La lectura de albaranes falla | Tesseract no instalado o sin los idiomas spa/cat; revisa app.ocr.tesseract-command. En Windows, TESSDATA_PREFIX debe apuntar a la carpeta con spa y cat. |
| Una tarifa larga da error al cabo de un minuto | Falta en nginx location = /api/invoices/scan con proxy_read_timeout 240s (el valor por defecto es 60 s). |
| El chat responde 401 | Sesión caducada (30 días sin usar la app) o cerrada al cambiar la contraseña en otro dispositivo: vuelve a entrar. |
| Tras desplegar se ven textos antiguos | Caché del navegador: recarga forzando (Ctrl+F5). En producción index.html e /i18n/ ya se sirven con no-cache. |
| No llegan las alertas de n8n | Flujo sin publicar o credencial SMTP incorrecta en n8n; comprueba también N8N_NEW_COMPANY_WEBHOOK en el .env de la API. |
| No llegan los emails | Revisa MAIL_USER_PEDIDAI / MAIL_PASS_PEDIDAI (contraseña de aplicación) y los logs: journalctl -u pedidai-api. |
Proyecto de DevEps. Todos los derechos reservados. Contacto: [email protected] · GitHub Issues
PENDINGSENTPENDINGCANCELLEDCONFIRMEDREJECTEDCOMPLETED?lang=ca o ?lang=es fija el idioma (lo usan los anuncios en catalán); si no, se usa el elegido antes o el idioma principal del navegador (catalán solo si es el primero; si no, castellano). El selector va en orden ES | CA.1.234,50 €) en todas las pantallas./extend-trial/activateindex.htmlinlineCritical: falsewithEventReplayimg-srcconnect-srcwww.googleadservices.comad.doubleclick.netframe-srcwww.youtube-nocookie.comX-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) desde un snippet de nginx, sin duplicarlas con las de Spring; nginx no muestra su versión.127.0.0.1 (SERVER_ADDRESS); desde fuera solo se llega a través de nginx.CORS_ALLOWED_ORIGINS), sin localhost.