{"id":45446442,"url":"https://github.com/ezearcich/taskforge","last_synced_at":"2026-02-22T04:02:20.020Z","repository":{"id":339719022,"uuid":"1163123317","full_name":"EzeArcich/TaskForge","owner":"EzeArcich","description":null,"archived":false,"fork":false,"pushed_at":"2026-02-21T06:28:50.000Z","size":153,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-02-21T12:39:10.761Z","etag":null,"topics":["api","google-calendar","google-calendar-api","hexagonal-architecture","laravel","laravel-framework","openai","php","queue","scheduler","testing","trello","trello-api"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EzeArcich.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-21T05:59:41.000Z","updated_at":"2026-02-21T06:28:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/EzeArcich/TaskForge","commit_stats":null,"previous_names":["ezearcich/taskforge"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/EzeArcich/TaskForge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EzeArcich%2FTaskForge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EzeArcich%2FTaskForge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EzeArcich%2FTaskForge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EzeArcich%2FTaskForge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EzeArcich","download_url":"https://codeload.github.com/EzeArcich/TaskForge/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EzeArcich%2FTaskForge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29704420,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-22T03:17:42.375Z","status":"ssl_error","status_checked_at":"2026-02-22T03:17:31.622Z","response_time":110,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","google-calendar","google-calendar-api","hexagonal-architecture","laravel","laravel-framework","openai","php","queue","scheduler","testing","trello","trello-api"],"created_at":"2026-02-22T04:02:16.228Z","updated_at":"2026-02-22T04:02:20.009Z","avatar_url":"https://github.com/EzeArcich.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# DailyPro\n[![Laravel](https://github.com/EzeArcich/TaskForge/actions/workflows/laravel.yml/badge.svg?branch=master)](https://github.com/EzeArcich/TaskForge/actions/workflows/laravel.yml)\n![license](https://img.shields.io/github/license/EzeArcich/TaskForge)\n![php](https://img.shields.io/badge/php-8.2%2B-blue)\n![laravel](https://img.shields.io/badge/laravel-12-red)\n\n**\"Copio-pego un roadmap -\u003e el sistema me crea calendario + tablero + recordatorios.\"**\n\nDailyPro elimina la friccion entre \"tengo un plan\" y \"lo estoy ejecutando\". Recibe un plan en texto libre, lo normaliza con IA, lo calendariza segun tu disponibilidad, y lo publica en Trello + Google Calendar con recordatorios diarios.\n\n## Stack\n\n- **Framework:** Laravel 12 (PHP 8.2+)\n- **DB:** MySQL (SQLite para tests)\n- **IA:** OpenAI API (gpt-4o-mini por defecto, con fallback opcional por cuota)\n- **Kanban:** Trello API\n- **Calendar:** Google Calendar API (OAuth)\n- **Queue:** Database driver (configurable)\n\n---\n\n## Arquitectura\n\n```\nHexagonal / Ports \u0026 Adapters\n============================\n\n┌─────────────────────────────────────────────────────┐\n│  HTTP Layer (Controllers / Requests / Resources)    │\n│  routes/api.php                                     │\n└──────────────────────┬──────────────────────────────┘\n                       │\n┌──────────────────────▼──────────────────────────────┐\n│  Application Layer                                  │\n│  ┌─────────────┐ ┌──────────────┐ ┌──────────────┐  │\n│  │ PlanService  │ │ Scheduler    │ │ PlanText     │  │\n│  │ (use cases)  │ │ Service      │ │ Hasher       │  │\n│  └──────┬───────┘ └──────────────┘ └──────────────┘  │\n│         │                                            │\n│  ┌──────▼───────────────────────────────────────┐    │\n│  │ Contracts (Ports / Interfaces)               │    │\n│  │  - AiNormalizerInterface                     │    │\n│  │  - KanbanProviderInterface                   │    │\n│  │  - CalendarProviderInterface                 │    │\n│  └──────────────────────────────────────────────┘    │\n└──────────────────────┬───────────────────────────────┘\n                       │\n┌──────────────────────▼──────────────────────────────┐\n│  Infrastructure (Adapters)                          │\n│  ┌───────────────┐ ┌────────────┐ ┌──────────────┐  │\n│  │ OpenAI        │ │ Trello     │ │ Google Cal   │  │\n│  │ Normalizer    │ │ Provider   │ │ Provider     │  │\n│  └───────────────┘ └────────────┘ └──────────────┘  │\n└─────────────────────────────────────────────────────┘\n\n┌─────────────────────────────────────────────────────┐\n│  Domain (Models / Enums)                            │\n│  Plan, PlanWeek, PlanTask                           │\n│  PlanStatus, TaskStatus, ValidationStatus           │\n└─────────────────────────────────────────────────────┘\n```\n\n### Patrones usados\n\n| Patron | Donde |\n|---|---|\n| **Hexagonal (Ports \u0026 Adapters)** | Contracts (ports) vs Infrastructure (adapters) |\n| **Strategy** | KanbanProviderFactory / CalendarProviderFactory |\n| **Factory** | Provider factories para instanciar por nombre |\n| **DTO** | CreatePlanDTO, RescheduleDTO, AvailabilitySlotDTO |\n| **Service Layer** | PlanService orquesta todos los use cases |\n| **Job/Queue** | PublishPlanJob, DailyRunJob |\n| **Idempotency** | Hash SHA-256 en plan creation + publish guard |\n\n---\n\n## Estructura de directorios\n\n```\napp/\n├── Application/\n│   ├── Contracts/          # Ports (interfaces)\n│   ├── DTOs/               # Data Transfer Objects\n│   └── Services/           # Use cases y servicios puros\n├── Domain/\n│   └── Enums/              # PlanStatus, TaskStatus, ValidationStatus\n├── Exceptions/             # Custom exceptions\n├── Http/\n│   ├── Controllers/        # API controllers\n│   ├── Requests/           # Form request validation\n│   └── Resources/          # API Resources (JSON transform)\n├── Infrastructure/\n│   ├── AI/                 # OpenAI adapter\n│   ├── Calendar/           # Google Calendar adapter + factory\n│   └── Kanban/             # Trello adapter + factory\n├── Jobs/                   # PublishPlanJob, DailyRunJob\n├── Mail/                   # DailyPlanMail\n└── Models/                 # Eloquent models\n\ntests/\n├── Fakes/                  # Fake implementations for testing\n├── Feature/                # Feature tests (endpoint tests)\n└── Unit/                   # Unit tests (pure services)\n```\n\n---\n\n## Esquema de Base de Datos\n\n```\nplans\n├── id (PK)\n├── hash (unique, SHA-256 para idempotencia)\n├── plan_text (text)\n├── settings (JSON)\n├── normalized_json (JSON, nullable)\n├── schedule (JSON, nullable)\n├── validation_status (enum: pending|valid|invalid|needs_input)\n├── publish_status (enum: draft|publishing|published|needs_update)\n├── trello_board_id (nullable)\n├── trello_board_url (nullable)\n├── google_calendar_id (nullable)\n└── timestamps\n\nplan_weeks\n├── id (PK)\n├── plan_id (FK -\u003e plans)\n├── week_number (int)\n├── goal (string)\n└── timestamps\n\nplan_tasks\n├── id (PK)\n├── plan_id (FK -\u003e plans)\n├── plan_week_id (FK -\u003e plan_weeks)\n├── title (string)\n├── estimate_hours (decimal)\n├── status (enum: pending|in_progress|done)\n├── scheduled_date (date, nullable)\n├── scheduled_start (time, nullable)\n├── scheduled_end (time, nullable)\n├── trello_card_id (nullable)\n├── google_event_id (nullable)\n└── timestamps\n\nintegrations\n├── id (PK)\n├── provider (string, ej: google)\n├── access_token (text)\n├── refresh_token (text, nullable)\n├── expires_at (timestamp, nullable)\n└── timestamps\n```\n\n---\n\n## Setup local\n\n### 1. Clonar e instalar\n\n```bash\ngit clone \u003crepo-url\u003e dailypro\ncd dailypro\ncomposer install\ncp .env.example .env\nphp artisan key:generate\n```\n\n### 2. Configurar DB\n\nEditar `.env`:\n\n```env\nDB_CONNECTION=mysql\nDB_HOST=127.0.0.1\nDB_PORT=3306\nDB_DATABASE=dailypro\nDB_USERNAME=root\nDB_PASSWORD=root\n```\n\n### 3. Correr migraciones\n\n```bash\nphp artisan migrate\n```\n\n### 4. Configurar variables de integracion\n\n```env\n# OpenAI\nOPENAI_API_KEY=sk-...\nOPENAI_MODEL=gpt-4o-mini\nDAILYPRO_OPENAI_QUOTA_FALLBACK=false\n\n# Trello (https://trello.com/power-ups/admin)\nTRELLO_KEY=your-trello-key\nTRELLO_TOKEN=your-trello-token\nTRELLO_WEBHOOK_SECRET=optional-secret\n\n# Google Calendar\nGOOGLE_CLIENT_ID=your-client-id\nGOOGLE_CLIENT_SECRET=your-client-secret\nGOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback\n# Opcional: fallback si no hay registro en integrations\nGOOGLE_ACCESS_TOKEN=your-oauth-token\n\n# Email de recordatorio\nDAILYPRO_REMINDER_EMAIL=you@example.com\n```\n\n### 5. Levantar servidor\n\n```bash\nphp artisan serve\n# O con queue worker:\nphp artisan serve \u0026 php artisan queue:work\n```\n\n---\n\n## Variables .env\n\n| Variable | Requerida | Descripcion |\n|---|---|---|\n| `OPENAI_API_KEY` | Si | API key de OpenAI |\n| `OPENAI_MODEL` | No | Modelo a usar (default: gpt-4o-mini) |\n| `DAILYPRO_OPENAI_QUOTA_FALLBACK` | No | Si es `true`, ante 429/sin key usa normalizador local de fallback |\n| `TRELLO_KEY` | Si | Trello API key |\n| `TRELLO_TOKEN` | Si | Trello API token |\n| `TRELLO_WEBHOOK_SECRET` | No | Secret para validar webhooks |\n| `GOOGLE_CLIENT_ID` | Si | Google OAuth client ID |\n| `GOOGLE_CLIENT_SECRET` | Si | Google OAuth client secret |\n| `GOOGLE_REDIRECT_URI` | Si | URI de callback OAuth |\n| `GOOGLE_ACCESS_TOKEN` | No | Fallback opcional si no hay token en `integrations` |\n| `DAILYPRO_REMINDER_EMAIL` | No | Email para recordatorios diarios |\n\n---\n\n## Correr tests\n\n```bash\n# Todos los tests\nphp artisan test\n\n# Solo unit tests\nphp artisan test --testsuite=Unit\n\n# Solo feature tests\nphp artisan test --testsuite=Feature\n\n# Un test especifico\nphp artisan test --filter=CreatePlanTest\n\n# Con coverage (requiere Xdebug/PCOV)\nphp artisan test --coverage\n```\n\nTests usan SQLite in-memory y fakes para todas las integraciones externas (OpenAI, Trello, Google Calendar).\n\n---\n\n## OAuth Google Calendar (pasos)\n\n1. Ir a [Google Cloud Console](https://console.cloud.google.com/)\n2. Crear proyecto o seleccionar existente\n3. Habilitar **Google Calendar API**\n4. Ir a **Credentials** \u003e **Create Credentials** \u003e **OAuth 2.0 Client ID**\n5. Application type: **Web application**\n6. Authorized redirect URIs: `http://localhost:8000/auth/google/callback`\n7. Copiar `Client ID` y `Client Secret` al `.env`\n8. Ejecutar OAuth local en el proyecto:\n   - Abrir `http://localhost:8000/auth/google`\n   - Completar consentimiento\n   - El callback `http://localhost:8000/auth/google/callback` guarda `access_token` + `refresh_token` en tabla `integrations`\n\nNotas:\n- En publish, el provider de Google usa primero `integrations` y solo si falta usa `GOOGLE_ACCESS_TOKEN`.\n- Si Google responde 401, intenta refresh automatico con `refresh_token` y reintenta una vez.\n- Si prefieres flujo manual, puedes usar OAuth Playground y guardar `GOOGLE_ACCESS_TOKEN` en `.env` como fallback.\n\n---\n\n## Flujo de uso\n\n```\n1. IMPORT    →  POST /api/plans          (enviar texto del plan)\n2. PREVIEW   →  Respuesta incluye normalized_json + schedule\n3. PUBLISH   →  POST /api/plans/{id}/publish  (crea Trello board + Calendar events)\n4. RESCHEDULE → POST /api/plans/{id}/reschedule  (cambia disponibilidad)\n5. DAILY RUN →  POST /api/plans/{id}/daily-run   (o automatico via scheduler)\n```\n\n---\n\n## API Endpoints\n\n### 1. Crear plan (POST /api/plans)\n\nIngesta y normalizacion del plan. Idempotente por hash.\n\n```bash\ncurl -X POST http://localhost:8000/api/plans \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json\" \\\n  -d '{\n    \"plan_text\": \"Semana 1: Aprender routing y controllers en Laravel. Hacer CRUD basico (3h). Leer docs oficiales (2h).\\nSemana 2: Eloquent ORM y migraciones. Crear modelos y relaciones (4h). Seeders y factories (1.5h).\\nSemana 3: Testing con PHPUnit. Unit tests (2h). Feature tests (2h). Mocking (1.5h).\\nSemana 4: Deploy. Configurar servidor (2h). CI/CD con GitHub Actions (2h). Monitoreo (1h).\",\n    \"settings\": {\n      \"timezone\": \"America/Argentina/Buenos_Aires\",\n      \"start_date\": \"2025-02-03\",\n      \"availability\": [\n        {\"day\": \"mon\", \"start\": \"20:00\", \"end\": \"21:30\"},\n        {\"day\": \"tue\", \"start\": \"20:00\", \"end\": \"21:30\"},\n        {\"day\": \"wed\", \"start\": \"20:00\", \"end\": \"21:30\"},\n        {\"day\": \"thu\", \"start\": \"20:00\", \"end\": \"21:30\"},\n        {\"day\": \"fri\", \"start\": \"20:00\", \"end\": \"21:30\"}\n      ],\n      \"hours_per_week\": 7.5,\n      \"kanban_provider\": \"trello\",\n      \"calendar_provider\": \"google\",\n      \"reminders\": {\"email\": true}\n    }\n  }'\n```\n\n**Respuesta exitosa (201 Created):**\n\n```json\n{\n  \"data\": {\n    \"id\": 1,\n    \"hash\": \"a1b2c3d4e5f6...\",\n    \"plan_text\": \"Semana 1: Aprender routing...\",\n    \"settings\": {\n      \"timezone\": \"America/Argentina/Buenos_Aires\",\n      \"start_date\": \"2025-02-03\",\n      \"availability\": [...],\n      \"hours_per_week\": 7.5,\n      \"kanban_provider\": \"trello\",\n      \"calendar_provider\": \"google\",\n      \"reminders\": {\"email\": true}\n    },\n    \"normalized_json\": {\n      \"title\": \"Plan de Estudio Laravel\",\n      \"timezone\": \"America/Argentina/Buenos_Aires\",\n      \"start_date\": \"2025-02-03\",\n      \"weeks\": [\n        {\n          \"week\": 1,\n          \"goal\": \"Aprender routing y controllers\",\n          \"tasks\": [\n            {\"title\": \"Hacer CRUD basico\", \"estimate_hours\": 3},\n            {\"title\": \"Leer docs oficiales\", \"estimate_hours\": 2}\n          ]\n        }\n      ]\n    },\n    \"schedule\": {\n      \"slots\": [\n        {\n          \"week\": 1,\n          \"task_title\": \"Hacer CRUD basico\",\n          \"date\": \"2025-02-03\",\n          \"start\": \"20:00\",\n          \"end\": \"21:30\",\n          \"minutes\": 90\n        }\n      ],\n      \"warnings\": []\n    },\n    \"validation_status\": \"valid\",\n    \"publish_status\": \"draft\",\n    \"publication\": {\n      \"trello\": {\"published\": false, \"board_id\": null, \"board_url\": null},\n      \"google_calendar\": {\"published\": false, \"calendar_id\": null}\n    },\n    \"weeks\": [...],\n    \"created_at\": \"2025-02-03T10:00:00+00:00\",\n    \"updated_at\": \"2025-02-03T10:00:00+00:00\"\n  }\n}\n```\n\n**Respuesta idempotente (200 OK):** Mismo body si el hash coincide.\n\n**Error de validacion (422):**\n\n```json\n{\n  \"message\": \"The plan text field must be at least 10 characters.\",\n  \"errors\": {\n    \"plan_text\": [\"The plan text field must be at least 10 characters.\"]\n  }\n}\n```\n\n**Error de normalizacion IA (422):**\n\n```json\n{\n  \"type\": \"normalization_error\",\n  \"title\": \"Plan Normalization Failed\",\n  \"detail\": \"AI normalization failed after 3 attempts.\",\n  \"errors\": {\"weeks.0.tasks\": [\"The weeks.0.tasks field is required.\"]}\n}\n```\n\n**Modo fallback por cuota OpenAI (opcional):**\n- Si `DAILYPRO_OPENAI_QUOTA_FALLBACK=true`, cuando OpenAI devuelve 429 (quota) o falta `OPENAI_API_KEY`,\n  el sistema genera un `normalized_json` base local y continua el flujo (`201/200`) para pruebas end-to-end.\n- Este modo es para desarrollo/testing; en produccion se recomienda usar normalizacion real con OpenAI.\n\n---\n\n### 2. Consultar plan (GET /api/plans/{id})\n\n```bash\ncurl -X GET http://localhost:8000/api/plans/1 \\\n  -H \"Accept: application/json\"\n```\n\n**Respuesta (200 OK):** Mismo formato que POST /plans.\n\n**Error (404):**\n\n```json\n{\n  \"message\": \"No query results for model [App\\\\Models\\\\Plan] 999\"\n}\n```\n\n---\n\n### 3. Publicar plan (POST /api/plans/{id}/publish)\n\nCrea tablero Trello + eventos Google Calendar. Idempotente.\n\n```bash\ncurl -X POST http://localhost:8000/api/plans/1/publish \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json\"\n```\n\n**Publicacion asincrona (via queue):**\n\n```bash\ncurl -X POST http://localhost:8000/api/plans/1/publish \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json\" \\\n  -d '{\"async\": true}'\n```\n\n**Respuesta sincrona exitosa (200):**\n\n```json\n{\n  \"data\": {\n    \"id\": 1,\n    \"publish_status\": \"published\",\n    \"publication\": {\n      \"trello\": {\n        \"published\": true,\n        \"board_id\": \"60a1b2c3d4e5f6\",\n        \"board_url\": \"https://trello.com/b/abc123/plan\"\n      },\n      \"google_calendar\": {\n        \"published\": true,\n        \"calendar_id\": \"primary\"\n      }\n    }\n  }\n}\n```\n\n**Respuesta asincrona (202):**\n\n```json\n{\n  \"message\": \"Plan publish queued.\",\n  \"plan_id\": 1,\n  \"status\": \"publishing\"\n}\n```\n\n**Error de publicacion (502):**\n\n```json\n{\n  \"type\": \"publish_error\",\n  \"title\": \"Publish Failed\",\n  \"detail\": \"Trello API returned 401 Unauthorized\"\n}\n```\n\nNotas de diagnostico rapido:\n- Si `publish_status=published` y existen `trello_card_id`/`google_event_id` en tareas, la publicacion fue exitosa.\n- Las tarjetas de Trello se crean en la lista **Backlog** del board nuevo.\n\n---\n\n### 4. Recalendarizar (POST /api/plans/{id}/reschedule)\n\n```bash\ncurl -X POST http://localhost:8000/api/plans/1/reschedule \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json\" \\\n  -d '{\n    \"availability\": [\n      {\"day\": \"mon\", \"start\": \"18:00\", \"end\": \"20:00\"},\n      {\"day\": \"wed\", \"start\": \"18:00\", \"end\": \"20:00\"},\n      {\"day\": \"fri\", \"start\": \"09:00\", \"end\": \"12:00\"}\n    ],\n    \"start_date\": \"2025-03-01\",\n    \"hours_per_week\": 10\n  }'\n```\n\n**Respuesta (200 OK):** Plan completo con schedule actualizado. Si estaba publicado, `publish_status` cambia a `\"needs_update\"`.\n\n---\n\n### 5. Webhook Trello (POST /api/webhooks/trello)\n\nRecibe callbacks de Trello cuando una card se mueve a \"Hecho\".\n\n```bash\ncurl -X POST http://localhost:8000/api/webhooks/trello?token=your-secret \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"action\": {\n      \"type\": \"updateCard\",\n      \"data\": {\n        \"card\": {\"id\": \"card_abc123\"},\n        \"listAfter\": {\"name\": \"Hecho\"},\n        \"listBefore\": {\"name\": \"Hoy\"}\n      }\n    }\n  }'\n```\n\n**Respuesta procesada (200):**\n\n```json\n{\"status\": \"processed\", \"task_updated\": true}\n```\n\n**Respuesta ignorada (200):**\n\n```json\n{\"status\": \"ignored\", \"reason\": \"irrelevant_action\"}\n```\n\n**Auth invalida (401):**\n\n```json\n{\"type\": \"authentication_error\", \"title\": \"Invalid webhook secret\"}\n```\n\n---\n\n### 6. Daily run (POST /api/plans/{id}/daily-run)\n\nEjecuta rutina diaria manualmente (mueve cards a \"Hoy\" + envia email).\n\n```bash\ncurl -X POST http://localhost:8000/api/plans/1/daily-run \\\n  -H \"Accept: application/json\"\n```\n\n**Respuesta (200):**\n\n```json\n{\n  \"message\": \"Daily run completed.\",\n  \"plan_id\": 1,\n  \"date\": \"2025-02-03\",\n  \"today_tasks\": [\n    {\n      \"id\": 1,\n      \"title\": \"Hacer CRUD basico\",\n      \"status\": \"pending\",\n      \"scheduled_start\": \"20:00\",\n      \"scheduled_end\": \"21:30\"\n    }\n  ]\n}\n```\n\n---\n\n## Scheduler (logica de calendarizacion)\n\nEl scheduler distribuye tareas en los bloques de disponibilidad declarados:\n\n- Itera semana por semana segun el plan normalizado\n- Construye slots disponibles para la semana (lun-dom segun `availability`)\n- Asigna tareas greedily: llena cada slot hasta completar `estimate_hours`\n- Si una tarea no entra en un slot, la divide en multiples slots\n- Si la semana no tiene suficiente disponibilidad, emite un `warning` de tipo `overflow`\n- Si una tarea queda sin asignar, emite un `warning` de tipo `unscheduled`\n\n---\n\n## Idempotencia\n\n- **Creacion:** Se calcula `SHA-256(plan_text_normalizado + settings_ordenados)`. Si el hash ya existe, devuelve el plan existente con status 200.\n- **Publicacion:** Si el plan ya esta publicado, devuelve el estado actual sin recrear tablero/eventos.\n- **Webhook:** Si la tarea ya esta en status `done`, el webhook no la modifica de nuevo.\n\n---\n\n## Jobs y Scheduler\n\n- `PublishPlanJob`: Publica a Trello + Calendar en background (3 reintentos, 30s backoff)\n- `DailyRunJob`: Mueve cards a \"Hoy\" + envia email\n- Cron schedule: `DailyRunJob` corre automaticamente a las 07:00 para todos los planes publicados\n\n```bash\n# Para activar el cron:\n* * * * * cd /path-to-project \u0026\u0026 php artisan schedule:run \u003e\u003e /dev/null 2\u003e\u00261\n```\n\n---\n\n## Manejo de errores\n\nErrores siguen estilo \"Problem Details\":\n\n| Codigo | Tipo | Cuando |\n|---|---|---|\n| 200 | OK | Operacion exitosa / idempotente |\n| 201 | Created | Plan creado por primera vez |\n| 202 | Accepted | Publish encolado (async) |\n| 401 | Auth error | Webhook secret invalido |\n| 404 | Not found | Plan no existe |\n| 422 | Validation | Input invalido o normalizacion IA fallida |\n| 502 | Bad gateway | Fallo en servicio externo (Trello/Google) |\n\n---\n\n## Troubleshooting rapido\n\n### 1) `POST /api/plans` devuelve 429 (OpenAI quota)\n\n**Sintoma:** `normalization_error` con detalle `OpenAI API returned status 429` o `You exceeded your current quota`.\n\n**Solucion:**\n- Activar fallback local en `.env`:\n  - `DAILYPRO_OPENAI_QUOTA_FALLBACK=true`\n- Limpiar cache y reiniciar:\n  - `php artisan optimize:clear`\n  - reiniciar `php artisan serve` / `composer run dev`\n\nCon eso, si OpenAI no responde por cuota, se genera `normalized_json` local y el plan se crea igual.\n\n### 2) `POST /api/plans/{id}/publish` devuelve 401 (Google)\n\n**Sintoma:** `publish_error` con `invalid authentication credentials`.\n\n**Causa comun:** access token vencido o invalido.\n\n**Solucion:**\n- Verificar `integrations` (`provider=google`) con `refresh_token` cargado.\n- Verificar `GOOGLE_CLIENT_ID` y `GOOGLE_CLIENT_SECRET` en `.env`.\n- Reintentar publish: el sistema refresca token automaticamente y reintenta una vez.\n- Si falla con `invalid_grant`, reautorizar en:\n  - `http://localhost:8000/auth/google`\n\n### 3) `POST /api/plans/{id}/publish` devuelve 400 (Google badRequest)\n\n**Sintoma:** `publish_error` con `HTTP request returned status code 400` desde Calendar.\n\n**Solucion:**\n- Asegurarte de estar corriendo la version actual (incluye normalizacion de `dateTime` para Google Calendar).\n- Reiniciar app despues de pull/cambios:\n  - `php artisan optimize:clear`\n  - reiniciar `php artisan serve` / `composer run dev`\n\n### 4) Publish responde `published` pero “no veo cards” en Trello\n\n**Chequeo rapido:**\n- Consultar `GET /api/plans/{id}` y validar:\n  - `publication.trello.published = true`\n  - `publication.trello.board_url` presente\n  - `weeks[].tasks[].trello_card_id` presente\n\n**Nota:** Las tarjetas se crean en la lista **Backlog** del board generado.\n\n---\n\n## Tests\n\n```\n83 tests, 189 assertions\n\nUnit (41 tests):\n- PlanTextHasherTest (9): hash consistency, normalization, SHA-256 format\n- NormalizedPlanValidatorTest (11): valid/invalid plans, all field validations\n- SchedulerServiceTest (10): scheduling, splitting, overflow, warnings\n- AvailabilitySlotDTOTest (4): from/to array, duration\n- KanbanProviderFactoryTest (4): supports, make, unknown\n- CalendarProviderFactoryTest (4): supports, make, unknown\n\nFeature (42 tests):\n- CreatePlanTest (13): success, idempotent, validation errors, AI failure\n- ShowPlanTest (3): full structure, publication status, 404\n- PublishPlanTest (6): success, idempotent, 404, 502, external IDs\n- ReschedulePlanTest (5): new availability, needs_update, validation, 404\n- TrelloWebhookTest (9): HEAD, mark done, idempotent, ignore, auth, empty\n- DailyRunTest (2): today tasks, 404\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fezearcich%2Ftaskforge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fezearcich%2Ftaskforge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fezearcich%2Ftaskforge/lists"}