diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cb9fa8c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,381 @@ +name: ERP Core CI + +on: + push: + branches: [main, develop, 'feature/*'] + pull_request: + branches: [main, develop] + +env: + NODE_VERSION: '20.x' + POSTGRES_DB: erp_generic_test + POSTGRES_USER: erp_admin + POSTGRES_PASSWORD: test_secret_2024 + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + +jobs: + # ========================================== + # Backend Tests + # ========================================== + backend-lint: + name: Backend Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + backend-unit-tests: + name: Backend Unit Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --testPathIgnorePatterns=integration + + - name: Upload coverage report + uses: codecov/codecov-action@v4 + if: always() + with: + directory: backend/coverage + flags: backend-unit + fail_ci_if_error: false + + backend-integration-tests: + name: Backend Integration Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: ${{ env.POSTGRES_DB }} + POSTGRES_USER: ${{ env.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Wait for PostgreSQL + run: | + until pg_isready -h ${{ env.POSTGRES_HOST }} -p ${{ env.POSTGRES_PORT }}; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + + - name: Setup test database + run: | + cd ../database + chmod +x scripts/create-test-database.sh + TEST_DB_NAME=${{ env.POSTGRES_DB }} \ + POSTGRES_USER=${{ env.POSTGRES_USER }} \ + POSTGRES_PASSWORD=${{ env.POSTGRES_PASSWORD }} \ + POSTGRES_HOST=${{ env.POSTGRES_HOST }} \ + POSTGRES_PORT=${{ env.POSTGRES_PORT }} \ + ./scripts/create-test-database.sh + + - name: Run integration tests + run: npm test -- --testPathPattern=integration + env: + TEST_DB_HOST: ${{ env.POSTGRES_HOST }} + TEST_DB_PORT: ${{ env.POSTGRES_PORT }} + TEST_DB_NAME: ${{ env.POSTGRES_DB }} + TEST_DB_USER: ${{ env.POSTGRES_USER }} + TEST_DB_PASSWORD: ${{ env.POSTGRES_PASSWORD }} + + backend-build: + name: Backend Build + runs-on: ubuntu-latest + needs: [backend-lint, backend-unit-tests] + defaults: + run: + working-directory: backend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: backend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: backend-dist + path: backend/dist + + # ========================================== + # Frontend Tests + # ========================================== + frontend-lint: + name: Frontend Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + frontend-unit-tests: + name: Frontend Unit Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --run + + - name: Upload coverage report + uses: codecov/codecov-action@v4 + if: always() + with: + directory: frontend/coverage + flags: frontend-unit + fail_ci_if_error: false + + frontend-e2e-tests: + name: Frontend E2E Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run E2E tests + run: npm run test:e2e + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: frontend/playwright-report + retention-days: 7 + + frontend-build: + name: Frontend Build + runs-on: ubuntu-latest + needs: [frontend-lint, frontend-unit-tests] + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: frontend/dist + + # ========================================== + # Database Validation + # ========================================== + database-validation: + name: Database DDL Validation + runs-on: ubuntu-latest + defaults: + run: + working-directory: database + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: erp_ddl_test + POSTGRES_USER: ${{ env.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Wait for PostgreSQL + run: | + until pg_isready -h localhost -p 5432; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + + - name: Validate DDL files can be executed + run: | + export PGPASSWORD=${{ env.POSTGRES_PASSWORD }} + + # Execute DDL files in order + for ddl_file in ddl/*.sql; do + echo "Executing $ddl_file..." + psql -h localhost -p 5432 -U ${{ env.POSTGRES_USER }} -d erp_ddl_test -f "$ddl_file" || exit 1 + done + + echo "All DDL files executed successfully!" + + - name: Check for schema objects + run: | + export PGPASSWORD=${{ env.POSTGRES_PASSWORD }} + + psql -h localhost -p 5432 -U ${{ env.POSTGRES_USER }} -d erp_ddl_test -c " + SELECT schemaname, COUNT(*) as tables + FROM pg_tables + WHERE schemaname NOT IN ('pg_catalog', 'information_schema') + GROUP BY schemaname + ORDER BY schemaname; + " + + # ========================================== + # Summary Job + # ========================================== + ci-success: + name: CI Success + runs-on: ubuntu-latest + needs: + - backend-build + - backend-integration-tests + - frontend-build + - frontend-e2e-tests + - database-validation + if: always() + + steps: + - name: Check all jobs status + run: | + if [ "${{ needs.backend-build.result }}" != "success" ] || \ + [ "${{ needs.backend-integration-tests.result }}" != "success" ] || \ + [ "${{ needs.frontend-build.result }}" != "success" ] || \ + [ "${{ needs.frontend-e2e-tests.result }}" != "success" ] || \ + [ "${{ needs.database-validation.result }}" != "success" ]; then + echo "One or more jobs failed" + exit 1 + fi + echo "All CI jobs passed successfully!" diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml new file mode 100644 index 0000000..4e95fca --- /dev/null +++ b/.github/workflows/performance.yml @@ -0,0 +1,231 @@ +name: Performance Tests + +on: + # Run on schedule (weekly on Sunday at midnight) + schedule: + - cron: '0 0 * * 0' + # Manual trigger + workflow_dispatch: + # Run on PRs to main (optional, can be heavy) + pull_request: + branches: [main] + paths: + - 'frontend/**' + - 'backend/**' + +env: + NODE_VERSION: '20.x' + +jobs: + # ========================================== + # Lighthouse Performance Audit + # ========================================== + lighthouse: + name: Lighthouse CI + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build production bundle + run: npm run build + + - name: Install Lighthouse CI + run: npm install -g @lhci/cli@0.13.x + + - name: Run Lighthouse CI + run: lhci autorun + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} + + - name: Upload Lighthouse results + uses: actions/upload-artifact@v4 + if: always() + with: + name: lighthouse-results + path: .lighthouseci + retention-days: 14 + + # ========================================== + # Bundle Size Analysis + # ========================================== + bundle-analysis: + name: Bundle Size Analysis + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build and analyze bundle + run: | + npm run build + echo "## Bundle Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Chunk Sizes" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + du -sh dist/assets/*.js | sort -h >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Total Size" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + du -sh dist >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + - name: Check bundle size limits + run: | + # Main bundle should be under 500KB + MAIN_SIZE=$(du -sb dist/assets/index*.js | cut -f1) + if [ "$MAIN_SIZE" -gt 512000 ]; then + echo "::warning::Main bundle exceeds 500KB limit" + fi + + # Total JS should be under 2MB + TOTAL_JS=$(du -sb dist/assets/*.js | awk '{sum+=$1} END {print sum}') + if [ "$TOTAL_JS" -gt 2097152 ]; then + echo "::warning::Total JS bundle exceeds 2MB limit" + fi + + # Report sizes + echo "Main bundle: $((MAIN_SIZE / 1024))KB" + echo "Total JS: $((TOTAL_JS / 1024))KB" + + # ========================================== + # k6 Load Tests + # ========================================== + load-tests: + name: k6 Load Tests + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: erp_generic_test + POSTGRES_USER: erp_admin + POSTGRES_PASSWORD: test_secret_2024 + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: backend/package-lock.json + + - name: Install k6 + run: | + sudo gpg -k + sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 + echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list + sudo apt-get update + sudo apt-get install k6 + + - name: Install backend dependencies + working-directory: backend + run: npm ci + + - name: Setup test database + run: | + cd database + chmod +x scripts/create-test-database.sh + TEST_DB_NAME=erp_generic_test \ + POSTGRES_USER=erp_admin \ + POSTGRES_PASSWORD=test_secret_2024 \ + POSTGRES_HOST=localhost \ + POSTGRES_PORT=5432 \ + ./scripts/create-test-database.sh + + - name: Start backend server + working-directory: backend + run: | + npm run build + npm start & + sleep 10 + env: + NODE_ENV: test + DB_HOST: localhost + DB_PORT: 5432 + DB_NAME: erp_generic_test + DB_USER: erp_admin + DB_PASSWORD: test_secret_2024 + JWT_SECRET: test-secret-key + PORT: 4000 + + - name: Run k6 smoke test + run: k6 run backend/tests/performance/load-test.js --vus 1 --duration 30s + env: + API_BASE_URL: http://localhost:4000 + + - name: Upload k6 results + uses: actions/upload-artifact@v4 + if: always() + with: + name: k6-results + path: backend/tests/performance/results/ + retention-days: 14 + + # ========================================== + # Performance Summary + # ========================================== + performance-summary: + name: Performance Summary + runs-on: ubuntu-latest + needs: [lighthouse, bundle-analysis] + if: always() + + steps: + - name: Download Lighthouse results + uses: actions/download-artifact@v4 + with: + name: lighthouse-results + path: lighthouse-results + continue-on-error: true + + - name: Generate summary + run: | + echo "# Performance Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Status" >> $GITHUB_STEP_SUMMARY + echo "- Lighthouse: ${{ needs.lighthouse.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Bundle Analysis: ${{ needs.bundle-analysis.result }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "See individual job artifacts for detailed results." >> $GITHUB_STEP_SUMMARY diff --git a/backend/package-lock.json b/backend/package-lock.json index 503d213..949af43 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,8 +20,10 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "node-cron": "^4.2.1", + "nodemailer": "^7.0.12", "otplib": "^12.0.1", "pg": "^8.11.3", + "puppeteer": "^22.15.0", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "socket.io": "^4.7.4", @@ -43,6 +45,7 @@ "@types/morgan": "^1.9.9", "@types/node": "^20.10.4", "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.4", "@types/pg": "^8.10.9", "@types/socket.io": "^3.0.0", "@types/supertest": "^6.0.2", @@ -106,11 +109,755 @@ "openapi-types": ">=7" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.966.0.tgz", + "integrity": "sha512-M1xu5gcGmaE1gGYHydODnlWz1YWgnzjfClrpzgCaLpWqGriH1dqFyGw0cyCV93jli0UbzyPrNVgb7aTphEjHvg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.966.0", + "@aws-sdk/credential-provider-node": "3.966.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.966.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/signature-v4-multi-region": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.966.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.1", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.2", + "@smithy/middleware-retry": "^4.4.18", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.17", + "@smithy/util-defaults-mode-node": "^4.2.20", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.966.0.tgz", + "integrity": "sha512-hQZDQgqRJclALDo9wK+bb5O+VpO8JcjImp52w9KPSz9XveNRgE9AYfklRJd8qT2Bwhxe6IbnqYEino2wqUMA1w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.966.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.966.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.966.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.1", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.2", + "@smithy/middleware-retry": "^4.4.18", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.17", + "@smithy/util-defaults-mode-node": "^4.2.20", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.966.0.tgz", + "integrity": "sha512-QaRVBHD1prdrFXIeFAY/1w4b4S0EFyo/ytzU+rCklEjMRT7DKGXGoHXTWLGz+HD7ovlS5u+9cf8a/LeSOEMzww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws-sdk/xml-builder": "3.965.0", + "@smithy/core": "^3.20.1", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.966.0.tgz", + "integrity": "sha512-sxVKc9PY0SH7jgN/8WxhbKQ7MWDIgaJv1AoAKJkhJ+GM5r09G5Vb2Vl8ALYpsy+r8b+iYpq5dGJj8k2VqxoQMg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.966.0.tgz", + "integrity": "sha512-VTJDP1jOibVtc5pn5TNE12rhqOO/n10IjkoJi8fFp9BMfmh3iqo70Ppvphz/Pe/R9LcK5Z3h0Z4EB9IXDR6kag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.966.0.tgz", + "integrity": "sha512-4oQKkYMCUx0mffKuH8LQag1M4Fo5daKVmsLAnjrIqKh91xmCrcWlAFNMgeEYvI1Yy125XeNSaFMfir6oNc2ODA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/credential-provider-env": "3.966.0", + "@aws-sdk/credential-provider-http": "3.966.0", + "@aws-sdk/credential-provider-login": "3.966.0", + "@aws-sdk/credential-provider-process": "3.966.0", + "@aws-sdk/credential-provider-sso": "3.966.0", + "@aws-sdk/credential-provider-web-identity": "3.966.0", + "@aws-sdk/nested-clients": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.966.0.tgz", + "integrity": "sha512-wD1KlqLyh23Xfns/ZAPxebwXixoJJCuDbeJHFrLDpP4D4h3vA2S8nSFgBSFR15q9FhgRfHleClycf6g5K4Ww6w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/nested-clients": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.966.0.tgz", + "integrity": "sha512-7QCOERGddMw7QbjE+LSAFgwOBpPv4px2ty0GCK7ZiPJGsni2EYmM4TtYnQb9u1WNHmHqIPWMbZR0pKDbyRyHlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.966.0", + "@aws-sdk/credential-provider-http": "3.966.0", + "@aws-sdk/credential-provider-ini": "3.966.0", + "@aws-sdk/credential-provider-process": "3.966.0", + "@aws-sdk/credential-provider-sso": "3.966.0", + "@aws-sdk/credential-provider-web-identity": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.966.0.tgz", + "integrity": "sha512-q5kCo+xHXisNbbPAh/DiCd+LZX4wdby77t7GLk0b2U0/mrel4lgy6o79CApe+0emakpOS1nPZS7voXA7vGPz4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.966.0.tgz", + "integrity": "sha512-Rv5aEfbpqsQZzxpX2x+FbSyVFOE3Dngome+exNA8jGzc00rrMZEUnm3J3yAsLp/I2l7wnTfI0r2zMe+T9/nZAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.966.0", + "@aws-sdk/core": "3.966.0", + "@aws-sdk/token-providers": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.966.0.tgz", + "integrity": "sha512-Yv1lc9iic9xg3ywMmIAeXN1YwuvfcClLVdiF2y71LqUgIOupW8B8my84XJr6pmOQuKzZa++c2znNhC9lGsbKyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/nested-clients": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.965.0.tgz", + "integrity": "sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.965.0.tgz", + "integrity": "sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.965.0.tgz", + "integrity": "sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.966.0.tgz", + "integrity": "sha512-9N9zncsY5ydDCRatKdrPZcdCwNWt7TdHmqgwQM52PuA5gs1HXWwLLNDy/51H+9RTHi7v6oly+x9utJ/qypCh2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-arn-parser": "3.966.0", + "@smithy/core": "^3.20.1", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.966.0.tgz", + "integrity": "sha512-MvGoy0vhMluVpSB5GaGJbYLqwbZfZjwEZhneDHdPhgCgQqmCtugnYIIjpUw7kKqWGsmaMQmNEgSFf1zYYmwOyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@smithy/core": "^3.20.1", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.966.0.tgz", + "integrity": "sha512-FRzAWwLNoKiaEWbYhnpnfartIdOgiaBLnPcd3uG1Io+vvxQUeRPhQIy4EfKnT3AuA+g7gzSCjMG2JKoJOplDtQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.966.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.966.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.966.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.1", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.2", + "@smithy/middleware-retry": "^4.4.18", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.3", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.17", + "@smithy/util-defaults-mode-node": "^4.2.20", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.965.0.tgz", + "integrity": "sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.966.0.tgz", + "integrity": "sha512-VNSpyfKtDiBg/nPwSXDvnjISaDE9mI8zhOK3C4/obqh8lK1V6j04xDlwyIWbbIM0f6VgV1FVixlghtJB79eBqA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.966.0.tgz", + "integrity": "sha512-8k5cBTicTGYJHhKaweO4gL4fud1KDnLS5fByT6/Xbiu59AxYM4E/h3ds+3jxDMnniCE3gIWpEnyfM9khtmw2lA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.966.0", + "@aws-sdk/nested-clients": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.965.0.tgz", + "integrity": "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.966.0.tgz", + "integrity": "sha512-WcCLdKBK2nHhtOPE8du5XjOXaOToxGF3Ge8rgK2jaRpjkzjS0/mO+Jp2H4+25hOne3sP2twBu5BrvD9KoXQ5LQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.965.0.tgz", + "integrity": "sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-endpoints": "^3.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.0.tgz", + "integrity": "sha512-9LJFand4bIoOjOF4x3wx0UZYiFZRo4oUauxQSiEX2dVg+5qeBOJSjp2SeWykIE6+6frCZ5wvWm2fGLK8D32aJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.965.0.tgz", + "integrity": "sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.966.0.tgz", + "integrity": "sha512-vPPe8V0GLj+jVS5EqFz2NUBgWH35favqxliUOvhp8xBdNRkEjiZm5TqitVtFlxS4RrLY3HOndrWbrP5ejbwl1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.966.0", + "@aws-sdk/types": "3.965.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.965.0.tgz", + "integrity": "sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -283,7 +1030,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1919,6 +2665,28 @@ "node": ">=14" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -1953,6 +2721,626 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", + "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", + "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.2.tgz", + "integrity": "sha512-nc99TseyTwL1bg+T21cyEA5oItNy1XN4aUeyOlXJnvyRW5VSK1oRKRoSM/Iq0KFPuqZMxjBemSZHZCOZbSyBMw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.8", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz", + "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz", + "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz", + "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz", + "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", + "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.3.tgz", + "integrity": "sha512-Zb8R35hjBhp1oFhiaAZ9QhClpPHdEDmNDC2UrrB2fqV0oNDUUPH12ovZHB5xi/Rd+pg/BJHOR1q+SfsieSKPQg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.20.2", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-middleware": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.19.tgz", + "integrity": "sha512-QtisFIjIw2tjMm/ESatjWFVIQb5Xd093z8xhxq/SijLg7Mgo2C2wod47Ib/AHpBLFhwYXPzd7Hp2+JVXfeZyMQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz", + "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz", + "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", + "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz", + "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", + "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz", + "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz", + "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz", + "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz", + "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", + "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz", + "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.10.4", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.4.tgz", + "integrity": "sha512-rHig+BWjhjlHlah67ryaW9DECYixiJo5pQCTEwsJyarRBAwHMMC3iYz5MXXAHXe64ZAMn1NhTUSTFIu1T6n6jg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.20.2", + "@smithy/middleware-endpoint": "^4.4.3", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", + "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz", + "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.18", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.18.tgz", + "integrity": "sha512-Ao1oLH37YmLyHnKdteMp6l4KMCGBeZEAN68YYe00KAaKFijFELDbRQRm3CNplz7bez1HifuBV0l5uR6eVJLhIg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.21.tgz", + "integrity": "sha512-e21ASJDirE96kKXZLcYcnn4Zt0WGOvMYc1P8EK0gQeQ3I8PbJWqBKx9AUr/YeFpDkpYwEu1RsPe4UXk2+QL7IA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.5", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", + "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", + "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz", + "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz", + "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@so-ric/colorspace": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", @@ -1975,6 +3363,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", "license": "MIT" }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2236,6 +3630,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/nodemailer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz", + "integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.15.6", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", @@ -2400,6 +3805,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -2652,6 +4067,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2783,6 +4207,18 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -2821,6 +4257,20 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2953,6 +4403,97 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz", + "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3010,6 +4551,15 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/basic-ftp": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", + "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -3055,6 +4605,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/bowser": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -3159,6 +4716,15 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3238,7 +4804,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3301,6 +4866,29 @@ "node": ">=10" } }, + "node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -3583,6 +5171,32 @@ "node": ">= 0.10" } }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -3619,6 +5233,15 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -3699,6 +5322,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3746,6 +5383,13 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -3886,6 +5530,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "6.5.5", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", @@ -3942,11 +5595,19 @@ } } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -4067,6 +5728,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", @@ -4201,7 +5883,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -4241,7 +5922,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -4265,6 +5945,15 @@ "node": ">= 0.6" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4376,6 +6065,41 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4383,6 +6107,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -4434,6 +6164,25 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -4454,6 +6203,15 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -4801,6 +6559,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5042,6 +6814,32 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5098,7 +6896,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -5183,6 +6980,15 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5196,7 +7002,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-callable": { @@ -6002,7 +7807,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -6041,7 +7845,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -6168,7 +7971,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -6489,6 +8291,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", @@ -6561,6 +8369,15 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-cron": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", @@ -6584,6 +8401,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6760,6 +8586,38 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -6770,7 +8628,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -6783,7 +8640,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -6879,6 +8735,12 @@ "node": ">=8" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/pg": { "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", @@ -6973,7 +8835,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7163,6 +9024,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -7190,12 +9060,50 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7206,6 +9114,63 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.15.0.tgz", + "integrity": "sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==", + "deprecated": "< 24.15.0 is no longer supported", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1312386", + "puppeteer-core": "22.15.0" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -7529,7 +9494,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -7972,6 +9936,16 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/socket.io": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", @@ -8051,11 +10025,39 @@ } } }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8151,6 +10153,17 @@ "node": ">= 0.8" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8261,6 +10274,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/superagent": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", @@ -8445,6 +10471,31 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -8484,6 +10535,15 @@ "node": "*" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -8505,6 +10565,12 @@ "node": ">=0.2.6" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -8879,7 +10945,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -8904,6 +10970,40 @@ "node": ">=0.8.0" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -8960,6 +11060,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9263,6 +11369,16 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index ced185e..6dc25a3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,8 +25,10 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "node-cron": "^4.2.1", + "nodemailer": "^7.0.12", "otplib": "^12.0.1", "pg": "^8.11.3", + "puppeteer": "^22.15.0", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "socket.io": "^4.7.4", @@ -48,6 +50,7 @@ "@types/morgan": "^1.9.9", "@types/node": "^20.10.4", "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.4", "@types/pg": "^8.10.9", "@types/socket.io": "^3.0.0", "@types/supertest": "^6.0.2", diff --git a/backend/src/docs/openapi.yaml b/backend/src/docs/openapi.yaml index 2b616d2..7177269 100644 --- a/backend/src/docs/openapi.yaml +++ b/backend/src/docs/openapi.yaml @@ -2,23 +2,32 @@ openapi: 3.0.0 info: title: ERP Generic - Core API description: | - API para el sistema ERP genérico multitenant. + API para el sistema ERP generico multitenant. - ## Características principales - - Autenticación JWT y gestión de sesiones + ## Caracteristicas principales + - Autenticacion JWT y gestion de sesiones - Multi-tenant con aislamiento de datos - - Gestión financiera y contable + - Gestion financiera y contable - Control de inventario y almacenes - Compras y ventas - - CRM y gestión de partners + - CRM y gestion de partners - Proyectos y recursos humanos - Sistema de permisos granular (API Keys) - ## Autenticación - Todos los endpoints requieren autenticación mediante Bearer Token (JWT). - Algunos endpoints administrativos pueden requerir API Key específica. + ## Autenticacion + Todos los endpoints requieren autenticacion mediante Bearer Token (JWT). + Algunos endpoints administrativos pueden requerir API Key especifica. - version: 0.1.0 + ## Roles + - `super_admin`: Acceso total al sistema + - `admin`: Administrador de tenant + - `manager`: Gerente con permisos amplios + - `sales`: Ventas + - `accountant`: Contabilidad + - `warehouse`: Almacen + - `user`: Usuario basico + + version: 1.0.0 contact: name: ERP Generic Support email: support@erpgeneric.com @@ -29,37 +38,45 @@ servers: - url: http://localhost:3003/api/v1 description: Desarrollo local - url: https://api.erpgeneric.com/api/v1 - description: Producción + description: Produccion tags: - name: Auth - description: Autenticación y autorización + description: Autenticacion y autorizacion + - name: MFA + description: Autenticacion Multi-Factor + - name: API Keys + description: Gestion de API Keys - name: Users - description: Gestión de usuarios + description: Gestion de usuarios - name: Companies - description: Gestión de empresas (tenants) - - name: Core - description: Configuración central y parámetros + description: Gestion de empresas (tenants) - name: Partners - description: Gestión de partners (clientes, proveedores, contactos) + description: Gestion de partners (clientes, proveedores, contactos) - name: Inventory description: Control de inventario y productos - name: Financial - description: Gestión financiera y contable + description: Gestion financiera y contable - name: Purchases - description: Compras y órdenes de compra + description: Compras y ordenes de compra - name: Sales description: Ventas, cotizaciones y pedidos - name: Projects - description: Gestión de proyectos y tareas - - name: System - description: Configuración del sistema y logs + description: Gestion de proyectos y tareas - name: CRM - description: CRM y gestión de oportunidades + description: CRM y gestion de oportunidades - name: HR description: Recursos humanos y empleados - name: Reports - description: Reportes y analíticas + description: Reportes y analiticas + - name: Dashboards + description: Dashboards y widgets + - name: Audit + description: Auditoria y logs del sistema + - name: System + description: Configuracion del sistema + - name: Health + description: Health checks y monitoreo components: securitySchemes: @@ -73,9 +90,10 @@ components: type: apiKey in: header name: X-API-Key - description: API Key para operaciones específicas + description: API Key para operaciones especificas schemas: + # Common Response Types ApiResponse: type: object properties: @@ -97,25 +115,541 @@ components: items: type: object pagination: + $ref: '#/components/schemas/Pagination' + + Pagination: + type: object + properties: + page: + type: integer + example: 1 + limit: + type: integer + example: 20 + total: + type: integer + example: 100 + totalPages: + type: integer + example: 5 + + Error: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: "Error message" + + # Auth Schemas + LoginRequest: + type: object + required: + - email + - password + properties: + email: + type: string + format: email + example: "user@example.com" + password: + type: string + format: password + example: "SecurePassword123" + + LoginResponse: + type: object + properties: + success: + type: boolean + example: true + data: type: object properties: - page: - type: integer - example: 1 - limit: - type: integer - example: 20 - total: - type: integer - example: 100 - totalPages: - type: integer - example: 5 + token: + type: string + description: JWT access token + refreshToken: + type: string + description: Token para renovar sesion + user: + $ref: '#/components/schemas/User' + + RegisterRequest: + type: object + required: + - email + - password + - firstName + - lastName + properties: + email: + type: string + format: email + password: + type: string + format: password + minLength: 8 + firstName: + type: string + lastName: + type: string + phone: + type: string + + ChangePasswordRequest: + type: object + required: + - currentPassword + - newPassword + properties: + currentPassword: + type: string + format: password + newPassword: + type: string + format: password + minLength: 8 + + # User Schema + User: + type: object + properties: + id: + type: string + format: uuid + email: + type: string + format: email + firstName: + type: string + lastName: + type: string + phone: + type: string + avatar: + type: string + role: + type: string + enum: [super_admin, admin, manager, sales, accountant, warehouse, user] + isActive: + type: boolean + emailVerified: + type: boolean + mfaEnabled: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + CreateUserRequest: + type: object + required: + - email + - password + - firstName + - lastName + properties: + email: + type: string + format: email + password: + type: string + format: password + firstName: + type: string + lastName: + type: string + phone: + type: string + role: + type: string + enum: [admin, manager, sales, accountant, warehouse, user] + default: user + + UpdateUserRequest: + type: object + properties: + firstName: + type: string + lastName: + type: string + phone: + type: string + avatar: + type: string + isActive: + type: boolean + + # Partner Schema + Partner: + type: object + properties: + id: + type: string + format: uuid + tenantId: + type: string + format: uuid + name: + type: string + displayName: + type: string + type: + type: string + enum: [company, individual] + isCustomer: + type: boolean + isSupplier: + type: boolean + isEmployee: + type: boolean + email: + type: string + format: email + phone: + type: string + mobile: + type: string + website: + type: string + taxId: + type: string + street: + type: string + street2: + type: string + city: + type: string + state: + type: string + zip: + type: string + countryCode: + type: string + creditLimit: + type: number + paymentTermId: + type: string + format: uuid + salesRepId: + type: string + format: uuid + isActive: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + CreatePartnerRequest: + type: object + required: + - name + properties: + name: + type: string + displayName: + type: string + type: + type: string + enum: [company, individual] + default: company + isCustomer: + type: boolean + default: false + isSupplier: + type: boolean + default: false + email: + type: string + format: email + phone: + type: string + mobile: + type: string + website: + type: string + taxId: + type: string + street: + type: string + city: + type: string + state: + type: string + zip: + type: string + countryCode: + type: string + creditLimit: + type: number + paymentTermId: + type: string + format: uuid + + # Product Schema + Product: + type: object + properties: + id: + type: string + format: uuid + tenantId: + type: string + format: uuid + code: + type: string + name: + type: string + description: + type: string + type: + type: string + enum: [product, service, consumable] + categoryId: + type: string + format: uuid + uomId: + type: string + format: uuid + listPrice: + type: number + standardCost: + type: number + weight: + type: number + volume: + type: number + saleOk: + type: boolean + purchaseOk: + type: boolean + isActive: + type: boolean + barcode: + type: string + trackLots: + type: boolean + trackSerials: + type: boolean + createdAt: + type: string + format: date-time + + CreateProductRequest: + type: object + required: + - name + - type + properties: + code: + type: string + name: + type: string + description: + type: string + type: + type: string + enum: [product, service, consumable] + categoryId: + type: string + format: uuid + uomId: + type: string + format: uuid + listPrice: + type: number + standardCost: + type: number + saleOk: + type: boolean + purchaseOk: + type: boolean + + # Warehouse Schema + Warehouse: + type: object + properties: + id: + type: string + format: uuid + tenantId: + type: string + format: uuid + code: + type: string + name: + type: string + address: + type: string + isActive: + type: boolean + + # Account Schema + Account: + type: object + properties: + id: + type: string + format: uuid + tenantId: + type: string + format: uuid + code: + type: string + name: + type: string + type: + type: string + enum: [asset, liability, equity, income, expense] + reconcile: + type: boolean + deprecated: + type: boolean + isActive: + type: boolean + + # Invoice Schema + Invoice: + type: object + properties: + id: + type: string + format: uuid + tenantId: + type: string + format: uuid + number: + type: string + partnerId: + type: string + format: uuid + type: + type: string + enum: [out_invoice, in_invoice, out_refund, in_refund] + state: + type: string + enum: [draft, posted, cancelled] + invoiceDate: + type: string + format: date + dueDate: + type: string + format: date + amountUntaxed: + type: number + amountTax: + type: number + amountTotal: + type: number + amountResidual: + type: number + currencyCode: + type: string + + # Employee Schema + Employee: + type: object + properties: + id: + type: string + format: uuid + tenantId: + type: string + format: uuid + employeeNumber: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + format: email + phone: + type: string + departmentId: + type: string + format: uuid + jobPositionId: + type: string + format: uuid + managerId: + type: string + format: uuid + hireDate: + type: string + format: date + status: + type: string + enum: [active, on_leave, terminated] + userId: + type: string + format: uuid + + # Department Schema + Department: + type: object + properties: + id: + type: string + format: uuid + tenantId: + type: string + format: uuid + name: + type: string + code: + type: string + parentId: + type: string + format: uuid + managerId: + type: string + format: uuid + isActive: + type: boolean + + # Dashboard Schema + Dashboard: + type: object + properties: + id: + type: string + format: uuid + tenantId: + type: string + format: uuid + name: + type: string + description: + type: string + isDefault: + type: boolean + isShared: + type: boolean + layout: + type: object + createdBy: + type: string + format: uuid + createdAt: + type: string + format: date-time security: - BearerAuth: [] paths: + # ==================== HEALTH ==================== /health: get: tags: @@ -136,3 +670,2098 @@ paths: timestamp: type: string format: date-time + + # ==================== AUTH ==================== + /auth/login: + post: + tags: + - Auth + summary: Iniciar sesion + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: Login exitoso + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '401': + description: Credenciales invalidas + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /auth/register: + post: + tags: + - Auth + summary: Registrar nuevo usuario + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + responses: + '201': + description: Usuario registrado exitosamente + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '400': + description: Datos invalidos o email ya registrado + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /auth/refresh: + post: + tags: + - Auth + summary: Renovar token de acceso + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - refreshToken + properties: + refreshToken: + type: string + responses: + '200': + description: Token renovado + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + token: + type: string + refreshToken: + type: string + + /auth/profile: + get: + tags: + - Auth + summary: Obtener perfil del usuario autenticado + responses: + '200': + description: Perfil del usuario + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + $ref: '#/components/schemas/User' + + /auth/change-password: + post: + tags: + - Auth + summary: Cambiar contrasena + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePasswordRequest' + responses: + '200': + description: Contrasena cambiada exitosamente + '400': + description: Contrasena actual incorrecta + + /auth/logout: + post: + tags: + - Auth + summary: Cerrar sesion actual + responses: + '200': + description: Sesion cerrada + + /auth/logout-all: + post: + tags: + - Auth + summary: Cerrar todas las sesiones + responses: + '200': + description: Todas las sesiones cerradas + + # ==================== MFA ==================== + /auth/mfa/setup: + post: + tags: + - MFA + summary: Configurar MFA (TOTP) + responses: + '200': + description: QR code y secret para configurar authenticator + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + secret: + type: string + qrCode: + type: string + description: Data URL de imagen QR + + /auth/mfa/verify: + post: + tags: + - MFA + summary: Verificar y activar MFA + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - code + properties: + code: + type: string + description: Codigo TOTP de 6 digitos + responses: + '200': + description: MFA activado exitosamente + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + backupCodes: + type: array + items: + type: string + + /auth/mfa/disable: + post: + tags: + - MFA + summary: Desactivar MFA + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - code + properties: + code: + type: string + responses: + '200': + description: MFA desactivado + + # ==================== API KEYS ==================== + /auth/api-keys: + get: + tags: + - API Keys + summary: Listar API Keys del usuario + responses: + '200': + description: Lista de API Keys + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + lastUsedAt: + type: string + format: date-time + expiresAt: + type: string + format: date-time + isActive: + type: boolean + + post: + tags: + - API Keys + summary: Crear nueva API Key + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + expiresAt: + type: string + format: date-time + permissions: + type: array + items: + type: string + responses: + '201': + description: API Key creada (el key solo se muestra una vez) + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: object + properties: + id: + type: string + key: + type: string + description: El API Key (guardar, no se mostrara de nuevo) + + /auth/api-keys/{id}: + delete: + tags: + - API Keys + summary: Revocar API Key + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: API Key revocada + + # ==================== USERS ==================== + /users: + get: + tags: + - Users + summary: Listar usuarios + description: Requiere rol admin o super_admin + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + - name: limit + in: query + schema: + type: integer + default: 20 + - name: search + in: query + schema: + type: string + - name: role + in: query + schema: + type: string + - name: isActive + in: query + schema: + type: boolean + responses: + '200': + description: Lista paginada de usuarios + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + + post: + tags: + - Users + summary: Crear usuario + description: Requiere rol admin o super_admin + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserRequest' + responses: + '201': + description: Usuario creado + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + $ref: '#/components/schemas/User' + + /users/{id}: + get: + tags: + - Users + summary: Obtener usuario por ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Datos del usuario + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + $ref: '#/components/schemas/User' + '404': + description: Usuario no encontrado + + put: + tags: + - Users + summary: Actualizar usuario + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRequest' + responses: + '200': + description: Usuario actualizado + + delete: + tags: + - Users + summary: Eliminar usuario + description: Requiere rol super_admin + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Usuario eliminado + + # ==================== PARTNERS ==================== + /partners: + get: + tags: + - Partners + summary: Listar partners + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + - name: limit + in: query + schema: + type: integer + default: 20 + - name: search + in: query + schema: + type: string + - name: type + in: query + schema: + type: string + enum: [company, individual] + - name: isCustomer + in: query + schema: + type: boolean + - name: isSupplier + in: query + schema: + type: boolean + - name: isActive + in: query + schema: + type: boolean + responses: + '200': + description: Lista paginada de partners + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Partner' + + post: + tags: + - Partners + summary: Crear partner + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePartnerRequest' + responses: + '201': + description: Partner creado + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + $ref: '#/components/schemas/Partner' + + /partners/customers: + get: + tags: + - Partners + summary: Listar solo clientes + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: search + in: query + schema: + type: string + responses: + '200': + description: Lista de clientes + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse' + + /partners/suppliers: + get: + tags: + - Partners + summary: Listar solo proveedores + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: search + in: query + schema: + type: string + responses: + '200': + description: Lista de proveedores + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedResponse' + + /partners/{id}: + get: + tags: + - Partners + summary: Obtener partner por ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Datos del partner + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + $ref: '#/components/schemas/Partner' + + put: + tags: + - Partners + summary: Actualizar partner + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePartnerRequest' + responses: + '200': + description: Partner actualizado + + delete: + tags: + - Partners + summary: Eliminar partner + description: Requiere rol admin o super_admin + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Partner eliminado + + # ==================== INVENTORY ==================== + /inventory/products: + get: + tags: + - Inventory + summary: Listar productos + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: search + in: query + schema: + type: string + - name: type + in: query + schema: + type: string + enum: [product, service, consumable] + - name: categoryId + in: query + schema: + type: string + format: uuid + - name: saleOk + in: query + schema: + type: boolean + - name: purchaseOk + in: query + schema: + type: boolean + responses: + '200': + description: Lista de productos + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Product' + + post: + tags: + - Inventory + summary: Crear producto + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProductRequest' + responses: + '201': + description: Producto creado + + /inventory/products/{id}: + get: + tags: + - Inventory + summary: Obtener producto por ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Datos del producto + + put: + tags: + - Inventory + summary: Actualizar producto + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProductRequest' + responses: + '200': + description: Producto actualizado + + delete: + tags: + - Inventory + summary: Eliminar producto + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Producto eliminado + + /inventory/products/{id}/stock: + get: + tags: + - Inventory + summary: Obtener stock del producto + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Stock por ubicacion + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: array + items: + type: object + properties: + locationId: + type: string + locationName: + type: string + quantity: + type: number + reservedQty: + type: number + availableQty: + type: number + + /inventory/warehouses: + get: + tags: + - Inventory + summary: Listar almacenes + responses: + '200': + description: Lista de almacenes + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: array + items: + $ref: '#/components/schemas/Warehouse' + + post: + tags: + - Inventory + summary: Crear almacen + description: Requiere rol admin o super_admin + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + - code + properties: + name: + type: string + code: + type: string + address: + type: string + responses: + '201': + description: Almacen creado + + /inventory/warehouses/{id}: + get: + tags: + - Inventory + summary: Obtener almacen por ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Datos del almacen + + put: + tags: + - Inventory + summary: Actualizar almacen + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Almacen actualizado + + delete: + tags: + - Inventory + summary: Eliminar almacen + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Almacen eliminado + + /inventory/warehouses/{id}/stock: + get: + tags: + - Inventory + summary: Obtener stock del almacen + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Stock por producto en el almacen + + /inventory/pickings: + get: + tags: + - Inventory + summary: Listar movimientos de inventario + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: state + in: query + schema: + type: string + enum: [draft, confirmed, assigned, done, cancel] + - name: pickingTypeId + in: query + schema: + type: string + format: uuid + responses: + '200': + description: Lista de pickings + + post: + tags: + - Inventory + summary: Crear picking + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - pickingTypeId + properties: + pickingTypeId: + type: string + format: uuid + partnerId: + type: string + format: uuid + scheduledDate: + type: string + format: date + origin: + type: string + lines: + type: array + items: + type: object + properties: + productId: + type: string + format: uuid + quantity: + type: number + locationId: + type: string + format: uuid + locationDestId: + type: string + format: uuid + responses: + '201': + description: Picking creado + + /inventory/pickings/{id}/confirm: + post: + tags: + - Inventory + summary: Confirmar picking + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Picking confirmado + + /inventory/pickings/{id}/validate: + post: + tags: + - Inventory + summary: Validar picking (completar) + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Picking validado + + /inventory/pickings/{id}/cancel: + post: + tags: + - Inventory + summary: Cancelar picking + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Picking cancelado + + # ==================== FINANCIAL ==================== + /financial/accounts: + get: + tags: + - Financial + summary: Listar cuentas contables + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: type + in: query + schema: + type: string + enum: [asset, liability, equity, income, expense] + - name: reconcile + in: query + schema: + type: boolean + responses: + '200': + description: Lista de cuentas + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Account' + + post: + tags: + - Financial + summary: Crear cuenta contable + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - code + - name + - type + properties: + code: + type: string + name: + type: string + type: + type: string + enum: [asset, liability, equity, income, expense] + reconcile: + type: boolean + responses: + '201': + description: Cuenta creada + + /financial/invoices: + get: + tags: + - Financial + summary: Listar facturas + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: type + in: query + schema: + type: string + enum: [out_invoice, in_invoice, out_refund, in_refund] + - name: state + in: query + schema: + type: string + enum: [draft, posted, cancelled] + - name: partnerId + in: query + schema: + type: string + format: uuid + responses: + '200': + description: Lista de facturas + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Invoice' + + post: + tags: + - Financial + summary: Crear factura + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - partnerId + - type + properties: + partnerId: + type: string + format: uuid + type: + type: string + enum: [out_invoice, in_invoice, out_refund, in_refund] + invoiceDate: + type: string + format: date + dueDate: + type: string + format: date + lines: + type: array + items: + type: object + properties: + productId: + type: string + format: uuid + name: + type: string + quantity: + type: number + priceUnit: + type: number + taxIds: + type: array + items: + type: string + format: uuid + responses: + '201': + description: Factura creada + + /financial/invoices/{id}: + get: + tags: + - Financial + summary: Obtener factura por ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Datos de la factura + + put: + tags: + - Financial + summary: Actualizar factura + description: Solo facturas en estado draft + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Factura actualizada + + delete: + tags: + - Financial + summary: Eliminar factura + description: Solo facturas en estado draft + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Factura eliminada + + /financial/invoices/{id}/post: + post: + tags: + - Financial + summary: Publicar factura + description: Cambia estado de draft a posted + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Factura publicada + + /financial/invoices/{id}/cancel: + post: + tags: + - Financial + summary: Cancelar factura + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Factura cancelada + + /financial/payments: + get: + tags: + - Financial + summary: Listar pagos + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: type + in: query + schema: + type: string + enum: [inbound, outbound] + - name: state + in: query + schema: + type: string + enum: [draft, posted, reconciled, cancelled] + responses: + '200': + description: Lista de pagos + + post: + tags: + - Financial + summary: Crear pago + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - partnerId + - amount + - paymentType + properties: + partnerId: + type: string + format: uuid + amount: + type: number + paymentType: + type: string + enum: [inbound, outbound] + paymentMethodId: + type: string + format: uuid + journalId: + type: string + format: uuid + paymentDate: + type: string + format: date + memo: + type: string + responses: + '201': + description: Pago creado + + /financial/payments/{id}/post: + post: + tags: + - Financial + summary: Publicar pago + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Pago publicado + + /financial/reconcile-models: + get: + tags: + - Financial + summary: Listar modelos de reconciliacion + responses: + '200': + description: Lista de modelos + + post: + tags: + - Financial + summary: Crear modelo de reconciliacion + responses: + '201': + description: Modelo creado + + # ==================== HR ==================== + /hr/employees: + get: + tags: + - HR + summary: Listar empleados + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: departmentId + in: query + schema: + type: string + format: uuid + - name: status + in: query + schema: + type: string + enum: [active, on_leave, terminated] + responses: + '200': + description: Lista de empleados + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Employee' + + post: + tags: + - HR + summary: Crear empleado + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - firstName + - lastName + properties: + firstName: + type: string + lastName: + type: string + email: + type: string + format: email + phone: + type: string + departmentId: + type: string + format: uuid + jobPositionId: + type: string + format: uuid + managerId: + type: string + format: uuid + hireDate: + type: string + format: date + responses: + '201': + description: Empleado creado + + /hr/employees/{id}: + get: + tags: + - HR + summary: Obtener empleado por ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Datos del empleado + + put: + tags: + - HR + summary: Actualizar empleado + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Empleado actualizado + + /hr/employees/{id}/terminate: + post: + tags: + - HR + summary: Terminar contrato del empleado + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + type: object + properties: + terminationDate: + type: string + format: date + reason: + type: string + responses: + '200': + description: Empleado terminado + + /hr/departments: + get: + tags: + - HR + summary: Listar departamentos + responses: + '200': + description: Lista de departamentos + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: array + items: + $ref: '#/components/schemas/Department' + + post: + tags: + - HR + summary: Crear departamento + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + code: + type: string + parentId: + type: string + format: uuid + managerId: + type: string + format: uuid + responses: + '201': + description: Departamento creado + + /hr/leaves: + get: + tags: + - HR + summary: Listar solicitudes de ausencia + parameters: + - name: employeeId + in: query + schema: + type: string + format: uuid + - name: state + in: query + schema: + type: string + enum: [draft, submitted, approved, rejected, cancelled] + responses: + '200': + description: Lista de ausencias + + post: + tags: + - HR + summary: Crear solicitud de ausencia + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - employeeId + - leaveTypeId + - dateFrom + - dateTo + properties: + employeeId: + type: string + format: uuid + leaveTypeId: + type: string + format: uuid + dateFrom: + type: string + format: date + dateTo: + type: string + format: date + reason: + type: string + responses: + '201': + description: Solicitud creada + + /hr/leaves/{id}/approve: + post: + tags: + - HR + summary: Aprobar solicitud de ausencia + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Solicitud aprobada + + /hr/leaves/{id}/reject: + post: + tags: + - HR + summary: Rechazar solicitud de ausencia + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Solicitud rechazada + + /hr/payslips: + get: + tags: + - HR + summary: Listar nominas + parameters: + - name: employeeId + in: query + schema: + type: string + format: uuid + - name: state + in: query + schema: + type: string + enum: [draft, verify, done, cancel] + responses: + '200': + description: Lista de nominas + + post: + tags: + - HR + summary: Crear nomina + responses: + '201': + description: Nomina creada + + /hr/payslips/{id}/verify: + post: + tags: + - HR + summary: Verificar nomina + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Nomina verificada + + /hr/payslips/{id}/confirm: + post: + tags: + - HR + summary: Confirmar nomina + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Nomina confirmada + + /hr/expense-sheets: + get: + tags: + - HR + summary: Listar hojas de gastos + responses: + '200': + description: Lista de hojas de gastos + + post: + tags: + - HR + summary: Crear hoja de gastos + responses: + '201': + description: Hoja creada + + /hr/expense-sheets/{id}/submit: + post: + tags: + - HR + summary: Enviar hoja de gastos para aprobacion + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Hoja enviada + + /hr/expense-sheets/{id}/approve: + post: + tags: + - HR + summary: Aprobar hoja de gastos + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Hoja aprobada + + # ==================== DASHBOARDS ==================== + /dashboards: + get: + tags: + - Dashboards + summary: Listar dashboards + responses: + '200': + description: Lista de dashboards + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + data: + type: array + items: + $ref: '#/components/schemas/Dashboard' + + post: + tags: + - Dashboards + summary: Crear dashboard + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + properties: + name: + type: string + description: + type: string + isShared: + type: boolean + layout: + type: object + responses: + '201': + description: Dashboard creado + + /dashboards/{id}: + get: + tags: + - Dashboards + summary: Obtener dashboard por ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Datos del dashboard + + put: + tags: + - Dashboards + summary: Actualizar dashboard + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Dashboard actualizado + + delete: + tags: + - Dashboards + summary: Eliminar dashboard + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Dashboard eliminado + + /dashboards/{id}/widgets: + get: + tags: + - Dashboards + summary: Listar widgets del dashboard + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Lista de widgets + + post: + tags: + - Dashboards + summary: Agregar widget al dashboard + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - type + - title + properties: + type: + type: string + enum: [kpi, chart, table, list] + title: + type: string + config: + type: object + position: + type: object + properties: + x: + type: integer + y: + type: integer + w: + type: integer + h: + type: integer + responses: + '201': + description: Widget agregado + + # ==================== REPORTS ==================== + /reports: + get: + tags: + - Reports + summary: Listar reportes disponibles + responses: + '200': + description: Lista de reportes + + /reports/definitions: + get: + tags: + - Reports + summary: Listar definiciones de reportes + responses: + '200': + description: Lista de definiciones + + post: + tags: + - Reports + summary: Crear definicion de reporte + responses: + '201': + description: Definicion creada + + /reports/executions: + get: + tags: + - Reports + summary: Listar ejecuciones de reportes + responses: + '200': + description: Lista de ejecuciones + + post: + tags: + - Reports + summary: Ejecutar reporte + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - definitionId + properties: + definitionId: + type: string + format: uuid + parameters: + type: object + responses: + '201': + description: Reporte ejecutado + + /reports/executions/{id}/export: + post: + tags: + - Reports + summary: Exportar reporte + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - format + properties: + format: + type: string + enum: [pdf, xlsx, csv, json, html] + responses: + '200': + description: Archivo exportado + content: + application/pdf: + schema: + type: string + format: binary + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: string + format: binary + + /reports/trial-balance: + get: + tags: + - Reports + summary: Obtener balance de comprobacion + parameters: + - name: dateFrom + in: query + schema: + type: string + format: date + - name: dateTo + in: query + schema: + type: string + format: date + responses: + '200': + description: Balance de comprobacion + + # ==================== AUDIT ==================== + /audit/logs: + get: + tags: + - Audit + summary: Listar logs de auditoria + parameters: + - name: page + in: query + schema: + type: integer + - name: limit + in: query + schema: + type: integer + - name: userId + in: query + schema: + type: string + format: uuid + - name: action + in: query + schema: + type: string + - name: entityType + in: query + schema: + type: string + - name: dateFrom + in: query + schema: + type: string + format: date + - name: dateTo + in: query + schema: + type: string + format: date + responses: + '200': + description: Lista de logs + + /audit/logs/{id}: + get: + tags: + - Audit + summary: Obtener detalle de log + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Detalle del log con cambios + + /security/events: + get: + tags: + - Audit + summary: Listar eventos de seguridad + parameters: + - name: type + in: query + schema: + type: string + enum: [login_success, login_failed, logout, password_change, mfa_enabled, mfa_disabled] + responses: + '200': + description: Lista de eventos + + # ==================== SYSTEM ==================== + /system/settings: + get: + tags: + - System + summary: Obtener configuracion del sistema + responses: + '200': + description: Configuracion actual + + put: + tags: + - System + summary: Actualizar configuracion + description: Requiere rol admin o super_admin + responses: + '200': + description: Configuracion actualizada diff --git a/backend/src/modules/crm/crm.controller.ts b/backend/src/modules/crm/crm.controller.ts index d69bce6..882fccd 100644 --- a/backend/src/modules/crm/crm.controller.ts +++ b/backend/src/modules/crm/crm.controller.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { leadsService, CreateLeadDto, UpdateLeadDto, LeadFilters } from './leads.service.js'; import { opportunitiesService, CreateOpportunityDto, UpdateOpportunityDto, OpportunityFilters } from './opportunities.service.js'; import { stagesService, CreateLeadStageDto, UpdateLeadStageDto, CreateOpportunityStageDto, UpdateOpportunityStageDto, CreateLostReasonDto, UpdateLostReasonDto } from './stages.service.js'; +import { tagsService, CreateTagDto, UpdateTagDto, TagFilters } from './tags.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; @@ -677,6 +678,94 @@ class CrmController { next(error); } } + + // ========== TAGS ========== + + async getTags(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filters: TagFilters = { + search: req.query.search as string | undefined, + page: parseInt(req.query.page as string) || 1, + limit: parseInt(req.query.limit as string) || 50, + }; + const result = await tagsService.getTags(req.tenantId!, filters); + res.json({ + success: true, + data: result.data, + meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 50)) }, + }); + } catch (error) { + next(error); + } + } + + async getTag(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const tag = await tagsService.getTagById(req.params.id, req.tenantId!); + res.json({ success: true, data: tag }); + } catch (error) { + next(error); + } + } + + async createTag(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const createTagSchema = z.object({ + name: z.string().min(1).max(100), + color: z.number().int().min(0).optional(), + }); + + const parseResult = createTagSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de tag invalidos', parseResult.error.errors); + } + + const dto: CreateTagDto = parseResult.data; + const tag = await tagsService.createTag(dto, req.tenantId!); + + res.status(201).json({ + success: true, + data: tag, + message: 'Tag creado exitosamente', + }); + } catch (error) { + next(error); + } + } + + async updateTag(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const updateTagSchema = z.object({ + name: z.string().min(1).max(100).optional(), + color: z.number().int().min(0).optional(), + }); + + const parseResult = updateTagSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de tag invalidos', parseResult.error.errors); + } + + const dto: UpdateTagDto = parseResult.data; + const tag = await tagsService.updateTag(req.params.id, dto, req.tenantId!); + + res.json({ + success: true, + data: tag, + message: 'Tag actualizado exitosamente', + }); + } catch (error) { + next(error); + } + } + + async deleteTag(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + await tagsService.deleteTag(req.params.id, req.tenantId!); + res.json({ success: true, message: 'Tag eliminado exitosamente' }); + } catch (error) { + next(error); + } + } } export const crmController = new CrmController(); diff --git a/backend/src/modules/crm/crm.routes.ts b/backend/src/modules/crm/crm.routes.ts index 8445ca9..d10ef13 100644 --- a/backend/src/modules/crm/crm.routes.ts +++ b/backend/src/modules/crm/crm.routes.ts @@ -123,4 +123,22 @@ router.delete('/lost-reasons/:id', requireRoles('admin', 'super_admin'), (req, r crmController.deleteLostReason(req, res, next) ); +// ========== TAGS ========== + +router.get('/tags', (req, res, next) => crmController.getTags(req, res, next)); + +router.get('/tags/:id', (req, res, next) => crmController.getTag(req, res, next)); + +router.post('/tags', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + crmController.createTag(req, res, next) +); + +router.put('/tags/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + crmController.updateTag(req, res, next) +); + +router.delete('/tags/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + crmController.deleteTag(req, res, next) +); + export default router; diff --git a/backend/src/modules/financial/entities/index.ts b/backend/src/modules/financial/entities/index.ts index 821f94d..0567f4e 100644 --- a/backend/src/modules/financial/entities/index.ts +++ b/backend/src/modules/financial/entities/index.ts @@ -21,3 +21,15 @@ export { Tax, TaxType } from './tax.entity.js'; // Fiscal period entities export { FiscalYear, FiscalPeriodStatus } from './fiscal-year.entity.js'; export { FiscalPeriod } from './fiscal-period.entity.js'; + +// Catalog entities (read-only) +export { Incoterm } from './incoterm.entity.js'; +export { PaymentMethodCatalog } from './payment-method.entity.js'; + +// Payment term entities +export { PaymentTerm } from './payment-term.entity.js'; +export { PaymentTermLine, PaymentTermValue } from './payment-term-line.entity.js'; + +// Reconcile model entities +export { ReconcileModel, ReconcileModelType } from './reconcile-model.entity.js'; +export { ReconcileModelLine } from './reconcile-model-line.entity.js'; diff --git a/backend/src/modules/financial/financial.controller.ts b/backend/src/modules/financial/financial.controller.ts index 4b91107..aea55e7 100644 --- a/backend/src/modules/financial/financial.controller.ts +++ b/backend/src/modules/financial/financial.controller.ts @@ -6,6 +6,10 @@ import { journalEntriesService, CreateJournalEntryDto, UpdateJournalEntryDto, Jo import { invoicesService, CreateInvoiceDto, UpdateInvoiceDto, CreateInvoiceLineDto, UpdateInvoiceLineDto, InvoiceFilters } from './invoices.service.js'; import { paymentsService, CreatePaymentDto, UpdatePaymentDto, ReconcileDto, PaymentFilters } from './payments.service.js'; import { taxesService, CreateTaxDto, UpdateTaxDto, TaxFilters } from './taxes.service.js'; +import { incotermsService, IncotermFilters } from './incoterms.service.js'; +import { paymentMethodsService, PaymentMethodFilters } from './payment-methods.service.js'; +import { paymentTermsService, CreatePaymentTermDto, UpdatePaymentTermDto, PaymentTermLineDto } from './payment-terms.service.js'; +import { reconcileModelsService, CreateReconcileModelDto, UpdateReconcileModelDto, ReconcileModelLineDto, ReconcileModelFilters } from './reconcile-models.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; @@ -262,6 +266,134 @@ const taxQuerySchema = z.object({ limit: z.coerce.number().int().positive().max(100).default(20), }); +// ========== INCOTERM SCHEMAS (read-only) ========== +const incotermQuerySchema = z.object({ + is_active: z.coerce.boolean().optional(), + search: z.string().optional(), + page: z.coerce.number().int().positive().default(1), + limit: z.coerce.number().int().positive().max(100).default(50), +}); + +// ========== PAYMENT METHOD SCHEMAS (read-only) ========== +const paymentMethodQuerySchema = z.object({ + payment_type: z.enum(['inbound', 'outbound']).optional(), + is_active: z.coerce.boolean().optional(), + search: z.string().optional(), + page: z.coerce.number().int().positive().default(1), + limit: z.coerce.number().int().positive().max(100).default(50), +}); + +// ========== PAYMENT TERMS SCHEMAS ========== +const createPaymentTermSchema = z.object({ + company_id: z.string().uuid(), + name: z.string().min(1).max(100), + code: z.string().min(1).max(20), + terms: z.array(z.object({ + days: z.number().int().min(0), + percent: z.number().min(0).max(100), + })).optional(), + active: z.boolean().default(true), + lines: z.array(z.object({ + value: z.enum(['balance', 'percent', 'fixed']).optional(), + value_amount: z.number().optional(), + nb_days: z.number().int().min(0).optional(), + delay_type: z.string().optional(), + day_of_the_month: z.number().int().min(1).max(31).nullable().optional(), + sequence: z.number().int().optional(), + })).optional(), +}); + +const updatePaymentTermSchema = z.object({ + name: z.string().min(1).max(100).optional(), + code: z.string().min(1).max(20).optional(), + terms: z.array(z.object({ + days: z.number().int().min(0), + percent: z.number().min(0).max(100), + })).optional(), + active: z.boolean().optional(), +}); + +const paymentTermQuerySchema = z.object({ + company_id: z.string().uuid().optional(), + active: z.coerce.boolean().optional(), + search: z.string().optional(), + page: z.coerce.number().int().positive().default(1), + limit: z.coerce.number().int().positive().max(100).default(50), +}); + +const paymentTermLineSchema = z.object({ + value: z.enum(['balance', 'percent', 'fixed']).optional(), + value_amount: z.number().optional(), + nb_days: z.number().int().min(0).optional(), + delay_type: z.string().optional(), + day_of_the_month: z.number().int().min(1).max(31).nullable().optional(), + sequence: z.number().int().optional(), +}); + +// ========== RECONCILE MODEL SCHEMAS ========== +const createReconcileModelSchema = z.object({ + name: z.string().min(1).max(255), + company_id: z.string().uuid().optional().nullable(), + sequence: z.number().int().optional(), + rule_type: z.enum(['writeoff_button', 'writeoff_suggestion', 'invoice_matching']).optional(), + auto_reconcile: z.boolean().optional(), + match_nature: z.enum(['amount_received', 'amount_paid', 'both']).optional(), + match_amount: z.enum(['lower', 'greater', 'between', 'any']).optional(), + match_amount_min: z.number().optional().nullable(), + match_amount_max: z.number().optional().nullable(), + match_label: z.string().optional().nullable(), + match_label_param: z.string().optional().nullable(), + match_partner: z.boolean().optional(), + match_partner_ids: z.array(z.string().uuid()).optional().nullable(), + is_active: z.boolean().default(true), + lines: z.array(z.object({ + account_id: z.string().uuid(), + journal_id: z.string().uuid().optional().nullable(), + label: z.string().optional().nullable(), + amount_type: z.enum(['percentage', 'fixed', 'regex']).optional(), + amount_value: z.number().optional(), + tax_ids: z.array(z.string().uuid()).optional().nullable(), + analytic_account_id: z.string().uuid().optional().nullable(), + sequence: z.number().int().optional(), + })).optional(), +}); + +const updateReconcileModelSchema = z.object({ + name: z.string().min(1).max(255).optional(), + sequence: z.number().int().optional(), + rule_type: z.enum(['writeoff_button', 'writeoff_suggestion', 'invoice_matching']).optional(), + auto_reconcile: z.boolean().optional(), + match_nature: z.enum(['amount_received', 'amount_paid', 'both']).optional(), + match_amount: z.enum(['lower', 'greater', 'between', 'any']).optional(), + match_amount_min: z.number().optional().nullable(), + match_amount_max: z.number().optional().nullable(), + match_label: z.string().optional().nullable(), + match_label_param: z.string().optional().nullable(), + match_partner: z.boolean().optional(), + match_partner_ids: z.array(z.string().uuid()).optional().nullable(), + is_active: z.boolean().optional(), +}); + +const reconcileModelQuerySchema = z.object({ + company_id: z.string().uuid().optional(), + rule_type: z.enum(['writeoff_button', 'writeoff_suggestion', 'invoice_matching']).optional(), + is_active: z.coerce.boolean().optional(), + search: z.string().optional(), + page: z.coerce.number().int().positive().default(1), + limit: z.coerce.number().int().positive().max(100).default(50), +}); + +const reconcileModelLineSchema = z.object({ + account_id: z.string().uuid(), + journal_id: z.string().uuid().optional().nullable(), + label: z.string().optional().nullable(), + amount_type: z.enum(['percentage', 'fixed', 'regex']).optional(), + amount_value: z.number().optional(), + tax_ids: z.array(z.string().uuid()).optional().nullable(), + analytic_account_id: z.string().uuid().optional().nullable(), + sequence: z.number().int().optional(), +}); + class FinancialController { // ========== ACCOUNT TYPES ========== async getAccountTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { @@ -777,6 +909,430 @@ class FinancialController { next(error); } } + + // ========== INCOTERMS (read-only) ========== + async getIncoterms(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const queryResult = incotermQuerySchema.safeParse(req.query); + if (!queryResult.success) { + throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); + } + const filters: IncotermFilters = toCamelCase(queryResult.data as Record); + const result = await incotermsService.findAll(filters); + res.json({ + success: true, + data: result.data, + meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 50)) }, + }); + } catch (error) { + next(error); + } + } + + async getIncoterm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const incoterm = await incotermsService.findById(req.params.id); + res.json({ success: true, data: incoterm }); + } catch (error) { + next(error); + } + } + + // ========== PAYMENT METHODS (read-only) ========== + async getPaymentMethods(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const queryResult = paymentMethodQuerySchema.safeParse(req.query); + if (!queryResult.success) { + throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); + } + const filters: PaymentMethodFilters = toCamelCase(queryResult.data as Record); + const result = await paymentMethodsService.findAll(filters); + res.json({ + success: true, + data: result.data, + meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 50)) }, + }); + } catch (error) { + next(error); + } + } + + async getPaymentMethod(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const paymentMethod = await paymentMethodsService.findById(req.params.id); + res.json({ success: true, data: paymentMethod }); + } catch (error) { + next(error); + } + } + + // ========== PAYMENT TERMS ========== + async getPaymentTerms(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const queryResult = paymentTermQuerySchema.safeParse(req.query); + if (!queryResult.success) { + throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); + } + const { company_id, active, search, page, limit } = queryResult.data; + const result = await paymentTermsService.findAll(req.tenantId!, { + companyId: company_id, + active, + search, + page, + limit, + }); + res.json({ + success: true, + data: result.data, + meta: { total: result.total, page: result.page, limit: result.limit, totalPages: Math.ceil(result.total / result.limit) }, + }); + } catch (error) { + next(error); + } + } + + async getPaymentTerm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const paymentTerm = await paymentTermsService.findById(req.params.id, req.tenantId!); + if (!paymentTerm) { + res.status(404).json({ success: false, error: 'Término de pago no encontrado' }); + return; + } + res.json({ success: true, data: paymentTerm }); + } catch (error) { + next(error); + } + } + + async createPaymentTerm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const parseResult = createPaymentTermSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de término de pago inválidos', parseResult.error.errors); + } + const dto: CreatePaymentTermDto = { + companyId: parseResult.data.company_id, + name: parseResult.data.name, + code: parseResult.data.code, + terms: parseResult.data.terms, + active: parseResult.data.active, + lines: parseResult.data.lines?.map(l => ({ + value: l.value, + valueAmount: l.value_amount, + nbDays: l.nb_days, + delayType: l.delay_type, + dayOfTheMonth: l.day_of_the_month, + sequence: l.sequence, + })), + }; + const paymentTerm = await paymentTermsService.create(req.tenantId!, dto, req.user!.userId); + res.status(201).json({ success: true, data: paymentTerm }); + } catch (error) { + next(error); + } + } + + async updatePaymentTerm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const parseResult = updatePaymentTermSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de término de pago inválidos', parseResult.error.errors); + } + const dto: UpdatePaymentTermDto = { + name: parseResult.data.name, + code: parseResult.data.code, + terms: parseResult.data.terms, + active: parseResult.data.active, + }; + const paymentTerm = await paymentTermsService.update(req.params.id, req.tenantId!, dto, req.user!.userId); + if (!paymentTerm) { + res.status(404).json({ success: false, error: 'Término de pago no encontrado' }); + return; + } + res.json({ success: true, data: paymentTerm }); + } catch (error) { + next(error); + } + } + + async deletePaymentTerm(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const deleted = await paymentTermsService.delete(req.params.id, req.tenantId!); + if (!deleted) { + res.status(404).json({ success: false, error: 'Término de pago no encontrado' }); + return; + } + res.json({ success: true, message: 'Término de pago eliminado' }); + } catch (error) { + next(error); + } + } + + // Payment Term Lines + async getPaymentTermLines(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const lines = await paymentTermsService.getLines(req.params.id, req.tenantId!); + res.json({ success: true, data: lines }); + } catch (error) { + next(error); + } + } + + async addPaymentTermLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const parseResult = paymentTermLineSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de línea inválidos', parseResult.error.errors); + } + const dto: PaymentTermLineDto = { + value: parseResult.data.value, + valueAmount: parseResult.data.value_amount, + nbDays: parseResult.data.nb_days, + delayType: parseResult.data.delay_type, + dayOfTheMonth: parseResult.data.day_of_the_month, + sequence: parseResult.data.sequence, + }; + const line = await paymentTermsService.addLine(req.params.id, req.tenantId!, dto); + if (!line) { + res.status(404).json({ success: false, error: 'Término de pago no encontrado' }); + return; + } + res.status(201).json({ success: true, data: line }); + } catch (error) { + next(error); + } + } + + async updatePaymentTermLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const parseResult = paymentTermLineSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de línea inválidos', parseResult.error.errors); + } + const dto: PaymentTermLineDto = { + value: parseResult.data.value, + valueAmount: parseResult.data.value_amount, + nbDays: parseResult.data.nb_days, + delayType: parseResult.data.delay_type, + dayOfTheMonth: parseResult.data.day_of_the_month, + sequence: parseResult.data.sequence, + }; + const line = await paymentTermsService.updateLine(req.params.id, req.params.lineId, req.tenantId!, dto); + if (!line) { + res.status(404).json({ success: false, error: 'Línea no encontrada' }); + return; + } + res.json({ success: true, data: line }); + } catch (error) { + next(error); + } + } + + async removePaymentTermLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const removed = await paymentTermsService.removeLine(req.params.id, req.params.lineId, req.tenantId!); + if (!removed) { + res.status(404).json({ success: false, error: 'Línea no encontrada' }); + return; + } + res.json({ success: true, message: 'Línea eliminada' }); + } catch (error) { + next(error); + } + } + + // ========== RECONCILE MODELS ========== + async getReconcileModels(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const queryResult = reconcileModelQuerySchema.safeParse(req.query); + if (!queryResult.success) { + throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); + } + const filters: ReconcileModelFilters = toCamelCase(queryResult.data as Record); + const result = await reconcileModelsService.findAll(req.tenantId!, filters); + res.json({ + success: true, + data: result.data, + meta: { total: result.total, page: result.page, limit: result.limit, totalPages: Math.ceil(result.total / result.limit) }, + }); + } catch (error) { + next(error); + } + } + + async getReconcileModel(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const model = await reconcileModelsService.findById(req.params.id, req.tenantId!); + if (!model) { + res.status(404).json({ success: false, error: 'Modelo de conciliación no encontrado' }); + return; + } + res.json({ success: true, data: model }); + } catch (error) { + next(error); + } + } + + async createReconcileModel(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const parseResult = createReconcileModelSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de modelo de conciliación inválidos', parseResult.error.errors); + } + const dto: CreateReconcileModelDto = { + name: parseResult.data.name, + companyId: parseResult.data.company_id, + sequence: parseResult.data.sequence, + ruleType: parseResult.data.rule_type, + autoReconcile: parseResult.data.auto_reconcile, + matchNature: parseResult.data.match_nature, + matchAmount: parseResult.data.match_amount, + matchAmountMin: parseResult.data.match_amount_min, + matchAmountMax: parseResult.data.match_amount_max, + matchLabel: parseResult.data.match_label, + matchLabelParam: parseResult.data.match_label_param, + matchPartner: parseResult.data.match_partner, + matchPartnerIds: parseResult.data.match_partner_ids, + isActive: parseResult.data.is_active, + lines: parseResult.data.lines?.map(l => ({ + accountId: l.account_id, + journalId: l.journal_id, + label: l.label, + amountType: l.amount_type, + amountValue: l.amount_value, + taxIds: l.tax_ids, + analyticAccountId: l.analytic_account_id, + sequence: l.sequence, + })), + }; + const model = await reconcileModelsService.create(req.tenantId!, dto); + res.status(201).json({ success: true, data: model, message: 'Modelo de conciliación creado exitosamente' }); + } catch (error) { + next(error); + } + } + + async updateReconcileModel(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const parseResult = updateReconcileModelSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de modelo de conciliación inválidos', parseResult.error.errors); + } + const dto: UpdateReconcileModelDto = { + name: parseResult.data.name, + sequence: parseResult.data.sequence, + ruleType: parseResult.data.rule_type, + autoReconcile: parseResult.data.auto_reconcile, + matchNature: parseResult.data.match_nature, + matchAmount: parseResult.data.match_amount, + matchAmountMin: parseResult.data.match_amount_min, + matchAmountMax: parseResult.data.match_amount_max, + matchLabel: parseResult.data.match_label, + matchLabelParam: parseResult.data.match_label_param, + matchPartner: parseResult.data.match_partner, + matchPartnerIds: parseResult.data.match_partner_ids, + isActive: parseResult.data.is_active, + }; + const model = await reconcileModelsService.update(req.params.id, req.tenantId!, dto); + if (!model) { + res.status(404).json({ success: false, error: 'Modelo de conciliación no encontrado' }); + return; + } + res.json({ success: true, data: model, message: 'Modelo de conciliación actualizado exitosamente' }); + } catch (error) { + next(error); + } + } + + async deleteReconcileModel(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const deleted = await reconcileModelsService.delete(req.params.id, req.tenantId!); + if (!deleted) { + res.status(404).json({ success: false, error: 'Modelo de conciliación no encontrado' }); + return; + } + res.json({ success: true, message: 'Modelo de conciliación eliminado exitosamente' }); + } catch (error) { + next(error); + } + } + + // Reconcile Model Lines + async getReconcileModelLines(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const lines = await reconcileModelsService.getLines(req.params.id, req.tenantId!); + res.json({ success: true, data: lines }); + } catch (error) { + next(error); + } + } + + async addReconcileModelLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const parseResult = reconcileModelLineSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de línea inválidos', parseResult.error.errors); + } + const dto: ReconcileModelLineDto = { + accountId: parseResult.data.account_id, + journalId: parseResult.data.journal_id, + label: parseResult.data.label, + amountType: parseResult.data.amount_type, + amountValue: parseResult.data.amount_value, + taxIds: parseResult.data.tax_ids, + analyticAccountId: parseResult.data.analytic_account_id, + sequence: parseResult.data.sequence, + }; + const line = await reconcileModelsService.addLine(req.params.id, req.tenantId!, dto); + if (!line) { + res.status(404).json({ success: false, error: 'Modelo de conciliación no encontrado' }); + return; + } + res.status(201).json({ success: true, data: line, message: 'Línea agregada exitosamente' }); + } catch (error) { + next(error); + } + } + + async updateReconcileModelLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const parseResult = reconcileModelLineSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de línea inválidos', parseResult.error.errors); + } + const dto: ReconcileModelLineDto = { + accountId: parseResult.data.account_id, + journalId: parseResult.data.journal_id, + label: parseResult.data.label, + amountType: parseResult.data.amount_type, + amountValue: parseResult.data.amount_value, + taxIds: parseResult.data.tax_ids, + analyticAccountId: parseResult.data.analytic_account_id, + sequence: parseResult.data.sequence, + }; + const line = await reconcileModelsService.updateLine(req.params.id, req.params.lineId, req.tenantId!, dto); + if (!line) { + res.status(404).json({ success: false, error: 'Línea no encontrada' }); + return; + } + res.json({ success: true, data: line, message: 'Línea actualizada exitosamente' }); + } catch (error) { + next(error); + } + } + + async removeReconcileModelLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const removed = await reconcileModelsService.removeLine(req.params.id, req.params.lineId, req.tenantId!); + if (!removed) { + res.status(404).json({ success: false, error: 'Línea no encontrada' }); + return; + } + res.json({ success: true, message: 'Línea eliminada exitosamente' }); + } catch (error) { + next(error); + } + } } export const financialController = new FinancialController(); diff --git a/backend/src/modules/financial/financial.routes.ts b/backend/src/modules/financial/financial.routes.ts index 8a18e65..7d4ad6d 100644 --- a/backend/src/modules/financial/financial.routes.ts +++ b/backend/src/modules/financial/financial.routes.ts @@ -147,4 +147,74 @@ router.delete('/taxes/:id', requireRoles('admin', 'super_admin'), (req, res, nex financialController.deleteTax(req, res, next) ); +// ========== INCOTERMS (read-only) ========== +router.get('/incoterms', (req, res, next) => financialController.getIncoterms(req, res, next)); +router.get('/incoterms/:id', (req, res, next) => financialController.getIncoterm(req, res, next)); + +// ========== PAYMENT METHODS (read-only) ========== +router.get('/payment-methods', (req, res, next) => financialController.getPaymentMethods(req, res, next)); +router.get('/payment-methods/:id', (req, res, next) => financialController.getPaymentMethod(req, res, next)); + +// ========== PAYMENT TERMS ========== +router.get('/payment-terms', requireRoles('admin', 'accountant', 'manager', 'sales', 'super_admin'), (req, res, next) => + financialController.getPaymentTerms(req, res, next) +); +router.get('/payment-terms/:id', requireRoles('admin', 'accountant', 'manager', 'sales', 'super_admin'), (req, res, next) => + financialController.getPaymentTerm(req, res, next) +); +router.post('/payment-terms', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.createPaymentTerm(req, res, next) +); +router.put('/payment-terms/:id', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.updatePaymentTerm(req, res, next) +); +router.delete('/payment-terms/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + financialController.deletePaymentTerm(req, res, next) +); + +// Payment Term Lines +router.get('/payment-terms/:id/lines', requireRoles('admin', 'accountant', 'manager', 'sales', 'super_admin'), (req, res, next) => + financialController.getPaymentTermLines(req, res, next) +); +router.post('/payment-terms/:id/lines', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.addPaymentTermLine(req, res, next) +); +router.put('/payment-terms/:id/lines/:lineId', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.updatePaymentTermLine(req, res, next) +); +router.delete('/payment-terms/:id/lines/:lineId', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.removePaymentTermLine(req, res, next) +); + +// ========== RECONCILE MODELS ========== +router.get('/reconcile-models', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.getReconcileModels(req, res, next) +); +router.get('/reconcile-models/:id', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.getReconcileModel(req, res, next) +); +router.post('/reconcile-models', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.createReconcileModel(req, res, next) +); +router.put('/reconcile-models/:id', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.updateReconcileModel(req, res, next) +); +router.delete('/reconcile-models/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + financialController.deleteReconcileModel(req, res, next) +); + +// Reconcile Model Lines +router.get('/reconcile-models/:id/lines', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.getReconcileModelLines(req, res, next) +); +router.post('/reconcile-models/:id/lines', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.addReconcileModelLine(req, res, next) +); +router.put('/reconcile-models/:id/lines/:lineId', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.updateReconcileModelLine(req, res, next) +); +router.delete('/reconcile-models/:id/lines/:lineId', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) => + financialController.removeReconcileModelLine(req, res, next) +); + export default router; diff --git a/backend/src/modules/hr/hr.routes.ts b/backend/src/modules/hr/hr.routes.ts index 68a78ed..4ee4d45 100644 --- a/backend/src/modules/hr/hr.routes.ts +++ b/backend/src/modules/hr/hr.routes.ts @@ -1,5 +1,6 @@ import { Router } from 'express'; import { hrController } from './hr.controller.js'; +import { hrExtendedController } from './hr-extended.controller.js'; import { authenticate, requireRoles } from '../../shared/middleware/auth.middleware.js'; const router = Router(); @@ -149,4 +150,187 @@ router.delete('/leaves/:id', requireRoles('admin', 'super_admin'), (req, res, ne hrController.deleteLeave(req, res, next) ); +// ========== SKILL TYPES ========== + +router.get('/skill-types', (req, res, next) => hrExtendedController.getSkillTypes(req, res, next)); + +router.get('/skill-types/:id', (req, res, next) => hrExtendedController.getSkillType(req, res, next)); + +router.post('/skill-types', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.createSkillType(req, res, next) +); + +router.put('/skill-types/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.updateSkillType(req, res, next) +); + +router.delete('/skill-types/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.deleteSkillType(req, res, next) +); + +// ========== SKILLS ========== + +router.get('/skills', (req, res, next) => hrExtendedController.getSkills(req, res, next)); + +router.get('/skills/:id', (req, res, next) => hrExtendedController.getSkill(req, res, next)); + +router.post('/skills', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.createSkill(req, res, next) +); + +router.put('/skills/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.updateSkill(req, res, next) +); + +router.delete('/skills/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.deleteSkill(req, res, next) +); + +// ========== SKILL LEVELS ========== + +router.get('/skill-levels', (req, res, next) => hrExtendedController.getSkillLevels(req, res, next)); + +router.post('/skill-levels', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.createSkillLevel(req, res, next) +); + +router.put('/skill-levels/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.updateSkillLevel(req, res, next) +); + +router.delete('/skill-levels/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.deleteSkillLevel(req, res, next) +); + +// ========== EMPLOYEE SKILLS ========== + +router.get('/employee-skills', (req, res, next) => hrExtendedController.getEmployeeSkills(req, res, next)); + +router.get('/employees/:employeeId/skills', (req, res, next) => + hrExtendedController.getEmployeeSkillsByEmployee(req, res, next) +); + +router.post('/employee-skills', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.createEmployeeSkill(req, res, next) +); + +router.put('/employee-skills/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.updateEmployeeSkill(req, res, next) +); + +router.delete('/employee-skills/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.deleteEmployeeSkill(req, res, next) +); + +// ========== EXPENSE SHEETS ========== + +router.get('/expense-sheets', (req, res, next) => hrExtendedController.getExpenseSheets(req, res, next)); + +router.get('/expense-sheets/:id', (req, res, next) => hrExtendedController.getExpenseSheet(req, res, next)); + +router.post('/expense-sheets', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.createExpenseSheet(req, res, next) +); + +router.put('/expense-sheets/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.updateExpenseSheet(req, res, next) +); + +router.post('/expense-sheets/:id/submit', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.submitExpenseSheet(req, res, next) +); + +router.post('/expense-sheets/:id/approve', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.approveExpenseSheet(req, res, next) +); + +router.post('/expense-sheets/:id/reject', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.rejectExpenseSheet(req, res, next) +); + +router.delete('/expense-sheets/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.deleteExpenseSheet(req, res, next) +); + +// ========== EXPENSES ========== + +router.get('/expenses', (req, res, next) => hrExtendedController.getExpenses(req, res, next)); + +router.get('/expenses/:id', (req, res, next) => hrExtendedController.getExpense(req, res, next)); + +router.post('/expenses', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.createExpense(req, res, next) +); + +router.put('/expenses/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.updateExpense(req, res, next) +); + +router.delete('/expenses/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.deleteExpense(req, res, next) +); + +// ========== PAYSLIP STRUCTURES ========== + +router.get('/payslip-structures', (req, res, next) => hrExtendedController.getPayslipStructures(req, res, next)); + +router.get('/payslip-structures/:id', (req, res, next) => hrExtendedController.getPayslipStructure(req, res, next)); + +router.post('/payslip-structures', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.createPayslipStructure(req, res, next) +); + +router.put('/payslip-structures/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.updatePayslipStructure(req, res, next) +); + +router.delete('/payslip-structures/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.deletePayslipStructure(req, res, next) +); + +// ========== PAYSLIPS ========== + +router.get('/payslips', (req, res, next) => hrExtendedController.getPayslips(req, res, next)); + +router.get('/payslips/:id', (req, res, next) => hrExtendedController.getPayslip(req, res, next)); + +router.get('/payslips/:id/lines', (req, res, next) => hrExtendedController.getPayslipLines(req, res, next)); + +router.post('/payslips', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.createPayslip(req, res, next) +); + +router.put('/payslips/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.updatePayslip(req, res, next) +); + +router.post('/payslips/:id/verify', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.verifyPayslip(req, res, next) +); + +router.post('/payslips/:id/confirm', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.confirmPayslip(req, res, next) +); + +router.post('/payslips/:id/cancel', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.cancelPayslip(req, res, next) +); + +router.delete('/payslips/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + hrExtendedController.deletePayslip(req, res, next) +); + +// Payslip Lines +router.post('/payslips/:id/lines', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.addPayslipLine(req, res, next) +); + +router.put('/payslips/:id/lines/:lineId', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.updatePayslipLine(req, res, next) +); + +router.delete('/payslips/:id/lines/:lineId', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) => + hrExtendedController.removePayslipLine(req, res, next) +); + export default router; diff --git a/backend/src/modules/hr/index.ts b/backend/src/modules/hr/index.ts index 1a5223b..2b7353c 100644 --- a/backend/src/modules/hr/index.ts +++ b/backend/src/modules/hr/index.ts @@ -2,5 +2,9 @@ export * from './employees.service.js'; export * from './departments.service.js'; export * from './contracts.service.js'; export * from './leaves.service.js'; +export * from './skills.service.js'; +export * from './expenses.service.js'; +export * from './payslips.service.js'; export * from './hr.controller.js'; +export * from './hr-extended.controller.js'; export { default as hrRoutes } from './hr.routes.js'; diff --git a/backend/src/modules/inventory/entities/index.ts b/backend/src/modules/inventory/entities/index.ts index 5a7df30..93ffa62 100644 --- a/backend/src/modules/inventory/entities/index.ts +++ b/backend/src/modules/inventory/entities/index.ts @@ -9,3 +9,4 @@ export * from './stock-move.entity.js'; export * from './inventory-adjustment.entity.js'; export * from './inventory-adjustment-line.entity.js'; export * from './stock-valuation-layer.entity.js'; +export * from './package-type.entity.js'; diff --git a/backend/src/modules/inventory/inventory.controller.ts b/backend/src/modules/inventory/inventory.controller.ts index 32d5ced..b5de48e 100644 --- a/backend/src/modules/inventory/inventory.controller.ts +++ b/backend/src/modules/inventory/inventory.controller.ts @@ -6,6 +6,7 @@ import { locationsService, CreateLocationDto, UpdateLocationDto, LocationFilters import { pickingsService, CreatePickingDto, PickingFilters } from './pickings.service.js'; import { lotsService, CreateLotDto, UpdateLotDto, LotFilters } from './lots.service.js'; import { adjustmentsService, CreateAdjustmentDto, UpdateAdjustmentDto, AdjustmentLineDto, UpdateAdjustmentLineDto, AdjustmentFilters } from './adjustments.service.js'; +import { packageTypesService, CreatePackageTypeDto, UpdatePackageTypeDto, PackageTypeFilters } from './package-types.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; @@ -931,6 +932,123 @@ class InventoryController { next(error); } } + + // ========== PACKAGE TYPES ========== + async getPackageTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const packageTypeQuerySchema = z.object({ + company_id: z.string().uuid().optional(), + search: z.string().optional(), + page: z.coerce.number().int().positive().default(1), + limit: z.coerce.number().int().positive().max(100).default(50), + }); + + const queryResult = packageTypeQuerySchema.safeParse(req.query); + if (!queryResult.success) { + throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); + } + + const filters = toCamelCase(queryResult.data as Record); + const result = await packageTypesService.findAll(req.tenantId!, filters); + + res.json({ + success: true, + data: result.data, + meta: { + total: result.total, + page: filters.page, + limit: filters.limit, + totalPages: Math.ceil(result.total / (filters.limit || 50)), + }, + }); + } catch (error) { + next(error); + } + } + + async getPackageType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const packageType = await packageTypesService.findById(req.params.id, req.tenantId!); + res.json({ success: true, data: packageType }); + } catch (error) { + next(error); + } + } + + async createPackageType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const createPackageTypeSchema = z.object({ + name: z.string().min(1).max(100), + sequence: z.number().int().optional(), + barcode: z.string().max(100).optional(), + height: z.number().positive().optional(), + width: z.number().positive().optional(), + packaging_length: z.number().positive().optional(), + base_weight: z.number().positive().optional(), + max_weight: z.number().positive().optional(), + shipper_package_code: z.string().max(50).optional(), + company_id: z.string().uuid().optional(), + }); + + const parseResult = createPackageTypeSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de tipo de empaque inválidos', parseResult.error.errors); + } + + const dto = toCamelCase(parseResult.data as Record); + const packageType = await packageTypesService.create(dto, req.tenantId!); + + res.status(201).json({ + success: true, + data: packageType, + message: 'Tipo de empaque creado exitosamente', + }); + } catch (error) { + next(error); + } + } + + async updatePackageType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const updatePackageTypeSchema = z.object({ + name: z.string().min(1).max(100).optional(), + sequence: z.number().int().optional(), + barcode: z.string().max(100).optional().nullable(), + height: z.number().positive().optional().nullable(), + width: z.number().positive().optional().nullable(), + packaging_length: z.number().positive().optional().nullable(), + base_weight: z.number().positive().optional().nullable(), + max_weight: z.number().positive().optional().nullable(), + shipper_package_code: z.string().max(50).optional().nullable(), + company_id: z.string().uuid().optional().nullable(), + }); + + const parseResult = updatePackageTypeSchema.safeParse(req.body); + if (!parseResult.success) { + throw new ValidationError('Datos de tipo de empaque inválidos', parseResult.error.errors); + } + + const dto = toCamelCase(parseResult.data as Record); + const packageType = await packageTypesService.update(req.params.id, dto, req.tenantId!); + + res.json({ + success: true, + data: packageType, + message: 'Tipo de empaque actualizado exitosamente', + }); + } catch (error) { + next(error); + } + } + + async deletePackageType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + await packageTypesService.delete(req.params.id, req.tenantId!); + res.json({ success: true, message: 'Tipo de empaque eliminado exitosamente' }); + } catch (error) { + next(error); + } + } } export const inventoryController = new InventoryController(); diff --git a/backend/src/modules/inventory/inventory.routes.ts b/backend/src/modules/inventory/inventory.routes.ts index 6f45bf6..eb66808 100644 --- a/backend/src/modules/inventory/inventory.routes.ts +++ b/backend/src/modules/inventory/inventory.routes.ts @@ -171,4 +171,21 @@ router.post('/valuation/consume', requireRoles('admin', 'manager', 'super_admin' valuationController.consumeFifo(req, res, next) ); +// ========== PACKAGE TYPES ========== +router.get('/package-types', (req, res, next) => inventoryController.getPackageTypes(req, res, next)); + +router.get('/package-types/:id', (req, res, next) => inventoryController.getPackageType(req, res, next)); + +router.post('/package-types', requireRoles('admin', 'manager', 'warehouse', 'super_admin'), (req, res, next) => + inventoryController.createPackageType(req, res, next) +); + +router.put('/package-types/:id', requireRoles('admin', 'manager', 'warehouse', 'super_admin'), (req, res, next) => + inventoryController.updatePackageType(req, res, next) +); + +router.delete('/package-types/:id', requireRoles('admin', 'super_admin'), (req, res, next) => + inventoryController.deletePackageType(req, res, next) +); + export default router; diff --git a/backend/src/modules/reports/reports.controller.ts b/backend/src/modules/reports/reports.controller.ts index 42e0286..6ec3bb0 100644 --- a/backend/src/modules/reports/reports.controller.ts +++ b/backend/src/modules/reports/reports.controller.ts @@ -2,6 +2,9 @@ import { Response, NextFunction } from 'express'; import { z } from 'zod'; import { AuthenticatedRequest } from '../../shared/types/index.js'; import { reportsService } from './reports.service.js'; +import { exportService, ExportFormat } from './export.service.js'; +import { generateTrialBalance as generateTrialBalanceTemplate } from './templates/report-templates.js'; +import { pdfService } from './pdf.service.js'; // ============================================================================ // VALIDATION SCHEMAS @@ -60,6 +63,13 @@ const generalLedgerSchema = z.object({ date_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), }); +const exportSchema = z.object({ + format: z.enum(['pdf', 'xlsx', 'csv', 'json', 'html']).default('pdf'), + title: z.string().optional(), + orientation: z.enum(['portrait', 'landscape']).optional(), + pageSize: z.enum(['A4', 'Letter', 'Legal']).optional(), +}); + // ============================================================================ // CONTROLLER // ============================================================================ @@ -429,6 +439,202 @@ class ReportsController { next(error); } } + + // ==================== EXPORT ==================== + + /** + * POST /reports/executions/:id/export + * Export an existing execution to a specific format + */ + async exportExecution( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const { id } = req.params; + const exportOptions = exportSchema.parse(req.body); + const tenantId = req.user!.tenantId; + + // Get the execution with results + const execution = await reportsService.findExecutionById(id, tenantId); + + if (!execution.result_data) { + res.status(400).json({ + success: false, + message: 'La ejecución no tiene datos de resultado', + }); + return; + } + + // Get definition for column config + const definition = await reportsService.findDefinitionById( + execution.definition_id, + tenantId + ); + + // Export the data + const result = await exportService.export(execution.result_data, { + format: exportOptions.format as ExportFormat, + title: exportOptions.title || definition.name, + columns: definition.columns_config || undefined, + orientation: exportOptions.orientation, + pageSize: exportOptions.pageSize, + includeTotals: true, + footerText: `Reporte: ${definition.name} | Ejecutado: ${execution.created_at}`, + }); + + // Set response headers for download + res.setHeader('Content-Type', result.mimeType); + res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); + res.setHeader('Content-Length', result.size); + + res.send(result.buffer); + } catch (error) { + next(error); + } + } + + /** + * GET /reports/quick/trial-balance/export + * Export trial balance directly to specified format + */ + async exportTrialBalance( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const params = trialBalanceSchema.parse(req.query); + const exportOptions = exportSchema.parse(req.query); + const tenantId = req.user!.tenantId; + + // Get the trial balance data + const data = await reportsService.generateTrialBalance( + tenantId, + params.company_id || null, + params.date_from, + params.date_to, + params.include_zero || false + ); + + // Calculate totals + const totals = { + initial_balance: 0, + debit: 0, + credit: 0, + final_balance: 0, + }; + + for (const row of data) { + totals.initial_balance += parseFloat(row.initial_debit) - parseFloat(row.initial_credit) || 0; + totals.debit += parseFloat(row.period_debit) || 0; + totals.credit += parseFloat(row.period_credit) || 0; + totals.final_balance += parseFloat(row.final_debit) - parseFloat(row.final_credit) || 0; + } + + // Transform data for template + const templateData = data.map(row => ({ + code: row.account_code, + name: row.account_name, + initial_balance: parseFloat(row.initial_debit) - parseFloat(row.initial_credit), + debit: parseFloat(row.period_debit), + credit: parseFloat(row.period_credit), + final_balance: parseFloat(row.final_debit) - parseFloat(row.final_credit), + })); + + // For PDF, use the specialized template + if (exportOptions.format === 'pdf') { + const html = generateTrialBalanceTemplate({ + title: 'Balanza de Comprobación', + subtitle: exportOptions.title, + companyName: 'ERP Core', + columns: [ + { key: 'code', label: 'Cuenta', align: 'left' }, + { key: 'name', label: 'Descripción', align: 'left' }, + { key: 'initial_balance', label: 'Saldo Inicial', align: 'right' }, + { key: 'debit', label: 'Debe', align: 'right' }, + { key: 'credit', label: 'Haber', align: 'right' }, + { key: 'final_balance', label: 'Saldo Final', align: 'right' }, + ], + data: templateData, + totals: { + initial_balance: totals.initial_balance, + debit: totals.debit, + credit: totals.credit, + final_balance: totals.final_balance, + }, + metadata: { + generatedAt: new Date().toLocaleString('es-MX'), + period: `${params.date_from} a ${params.date_to}`, + }, + currency: 'MXN', + fiscalPeriod: `${params.date_from} al ${params.date_to}`, + }); + + const pdfResult = await pdfService.generateFromHtml(html, { + orientation: exportOptions.orientation || 'landscape', + pageSize: exportOptions.pageSize || 'A4', + displayHeaderFooter: true, + }); + + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', 'attachment; filename="balanza-comprobacion.pdf"'); + res.setHeader('Content-Length', pdfResult.buffer.length); + res.send(pdfResult.buffer); + return; + } + + // For other formats, use the generic export service + const result = await exportService.export(templateData, { + format: exportOptions.format as ExportFormat, + title: 'Balanza de Comprobación', + subtitle: `Período: ${params.date_from} a ${params.date_to}`, + columns: [ + { key: 'code', label: 'Cuenta', align: 'left' }, + { key: 'name', label: 'Descripción', align: 'left' }, + { key: 'initial_balance', label: 'Saldo Inicial', align: 'right', format: 'currency' }, + { key: 'debit', label: 'Debe', align: 'right', format: 'currency' }, + { key: 'credit', label: 'Haber', align: 'right', format: 'currency' }, + { key: 'final_balance', label: 'Saldo Final', align: 'right', format: 'currency' }, + ], + includeTotals: true, + orientation: exportOptions.orientation, + pageSize: exportOptions.pageSize, + }); + + res.setHeader('Content-Type', result.mimeType); + res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); + res.setHeader('Content-Length', result.size); + res.send(result.buffer); + } catch (error) { + next(error); + } + } + + /** + * GET /reports/pdf/health + * Check PDF service health + */ + async checkPdfHealth( + req: AuthenticatedRequest, + res: Response, + next: NextFunction + ): Promise { + try { + const health = await pdfService.healthCheck(); + + res.json({ + success: true, + data: { + pdf_service: health.available ? 'available' : 'unavailable', + error: health.error, + }, + }); + } catch (error) { + next(error); + } + } } export const reportsController = new ReportsController(); diff --git a/backend/src/modules/reports/reports.routes.ts b/backend/src/modules/reports/reports.routes.ts index fa3c71e..b641fc4 100644 --- a/backend/src/modules/reports/reports.routes.ts +++ b/backend/src/modules/reports/reports.routes.ts @@ -16,11 +16,25 @@ router.get('/quick/trial-balance', (req, res, next) => reportsController.getTrialBalance(req, res, next) ); +router.get('/quick/trial-balance/export', + requireRoles('admin', 'manager', 'accountant', 'super_admin'), + (req, res, next) => reportsController.exportTrialBalance(req, res, next) +); + router.get('/quick/general-ledger', requireRoles('admin', 'manager', 'accountant', 'super_admin'), (req, res, next) => reportsController.getGeneralLedger(req, res, next) ); +// ============================================================================ +// PDF SERVICE +// ============================================================================ + +router.get('/pdf/health', + requireRoles('admin', 'super_admin'), + (req, res, next) => reportsController.checkPdfHealth(req, res, next) +); + // ============================================================================ // DEFINITIONS // ============================================================================ @@ -65,6 +79,12 @@ router.get('/executions/:id', (req, res, next) => reportsController.findExecutionById(req, res, next) ); +// Export execution results +router.post('/executions/:id/export', + requireRoles('admin', 'manager', 'accountant', 'super_admin'), + (req, res, next) => reportsController.exportExecution(req, res, next) +); + // ============================================================================ // SCHEDULES // ============================================================================ diff --git a/backend/src/modules/sales/orders.service.ts b/backend/src/modules/sales/orders.service.ts index cca04fc..95e95bc 100644 --- a/backend/src/modules/sales/orders.service.ts +++ b/backend/src/modules/sales/orders.service.ts @@ -459,12 +459,21 @@ class OrdersService { values.push(dto.analytic_account_id); } - // Recalculate amounts - const subtotal = quantity * priceUnit; - const discountAmount = subtotal * discount / 100; - const amountUntaxed = subtotal - discountAmount; - const amountTax = 0; // TODO: Calculate taxes - const amountTotal = amountUntaxed + amountTax; + // Recalculate amounts using taxesService + const taxIds = dto.tax_ids ?? existingLine.tax_ids; + const taxResult = await taxesService.calculateTaxes( + { + quantity, + priceUnit, + discount, + taxIds, + }, + tenantId, + 'sales' + ); + const amountUntaxed = taxResult.amountUntaxed; + const amountTax = taxResult.amountTax; + const amountTotal = taxResult.amountTotal; updateFields.push(`amount_untaxed = $${paramIndex++}`); values.push(amountUntaxed); diff --git a/backend/src/modules/sales/quotations.service.ts b/backend/src/modules/sales/quotations.service.ts index 9485e14..f9aef2f 100644 --- a/backend/src/modules/sales/quotations.service.ts +++ b/backend/src/modules/sales/quotations.service.ts @@ -1,6 +1,8 @@ import { query, queryOne, getClient } from '../../config/database.js'; import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; import { taxesService } from '../financial/taxes.service.js'; +import { emailService } from '../../shared/services/email.service.js'; +import { logger } from '../../shared/utils/logger.js'; export interface QuotationLine { id: string; @@ -409,12 +411,21 @@ class QuotationsService { values.push(dto.tax_ids); } - // Recalculate amounts - const subtotal = quantity * priceUnit; - const discountAmount = subtotal * discount / 100; - const amountUntaxed = subtotal - discountAmount; - const amountTax = 0; // TODO: Calculate taxes - const amountTotal = amountUntaxed + amountTax; + // Recalculate amounts using taxesService + const taxIds = dto.tax_ids ?? existingLine.tax_ids; + const taxResult = await taxesService.calculateTaxes( + { + quantity, + priceUnit, + discount, + taxIds, + }, + tenantId, + 'sales' + ); + const amountUntaxed = taxResult.amountUntaxed; + const amountTax = taxResult.amountTax; + const amountTotal = taxResult.amountTotal; updateFields.push(`amount_untaxed = $${paramIndex++}`); values.push(amountUntaxed); @@ -475,11 +486,257 @@ class QuotationsService { [userId, id, tenantId] ); - // TODO: Send email notification + // Send email notification to partner + await this.sendQuotationEmail(quotation, tenantId); return this.findById(id, tenantId); } + /** + * Send quotation email to partner + */ + private async sendQuotationEmail(quotation: Quotation, tenantId: string): Promise { + try { + // Get partner email + const partner = await queryOne<{ name: string; email: string | null }>( + `SELECT name, email FROM core.partners WHERE id = $1 AND tenant_id = $2`, + [quotation.partner_id, tenantId] + ); + + if (!partner?.email) { + logger.warn('Partner has no email, skipping quotation notification', { + quotationId: quotation.id, + partnerId: quotation.partner_id, + }); + return; + } + + // Get company info for the email + const company = await queryOne<{ name: string }>( + `SELECT name FROM auth.companies WHERE id = $1`, + [quotation.company_id] + ); + + const html = this.generateQuotationEmailTemplate({ + quotationName: quotation.name, + partnerName: partner.name, + companyName: company?.name || 'ERP Core', + validityDate: quotation.validity_date, + amountTotal: quotation.amount_total, + currencyCode: quotation.currency_code || 'MXN', + lines: quotation.lines || [], + }); + + const result = await emailService.send({ + to: partner.email, + subject: `Cotización ${quotation.name} - ${company?.name || 'ERP Core'}`, + html, + text: `Estimado/a ${partner.name},\n\nLe enviamos la cotización ${quotation.name} por un total de ${quotation.currency_code || 'MXN'} ${quotation.amount_total.toLocaleString()}.\n\nEsta cotización es válida hasta ${new Date(quotation.validity_date).toLocaleDateString('es-MX')}.\n\nSaludos,\n${company?.name || 'ERP Core'}`, + }); + + if (result.success) { + logger.info('Quotation email sent successfully', { + quotationId: quotation.id, + partnerEmail: partner.email, + messageId: result.messageId, + }); + } else { + logger.error('Failed to send quotation email', { + quotationId: quotation.id, + error: result.error, + }); + } + } catch (error) { + // Don't fail the send operation if email fails + logger.error('Error sending quotation email', { + quotationId: quotation.id, + error: (error as Error).message, + }); + } + } + + /** + * Generate HTML template for quotation email + */ + private generateQuotationEmailTemplate(params: { + quotationName: string; + partnerName: string; + companyName: string; + validityDate: Date; + amountTotal: number; + currencyCode: string; + lines: QuotationLine[]; + }): string { + const { quotationName, partnerName, companyName, validityDate, amountTotal, currencyCode, lines } = params; + const supportEmail = process.env.SUPPORT_EMAIL || 'ventas@erp-core.local'; + + const linesHtml = lines.map(line => ` + + ${line.description} + ${line.quantity} + ${currencyCode} ${line.price_unit.toLocaleString()} + ${currencyCode} ${line.amount_total.toLocaleString()} + + `).join(''); + + return ` + + + + + + Cotización ${quotationName} + + + +
+
+ +
+ +

Cotización ${quotationName}

+ +

Estimado/a ${partnerName},

+ +

Le enviamos nuestra cotización según lo solicitado. A continuación encontrará el detalle de los productos y/o servicios cotizados:

+ + + + + + + + + + + + ${linesHtml} + + + + + +
DescripciónCantidadPrecio Unit.Subtotal
TOTAL:${currencyCode} ${amountTotal.toLocaleString()}
+ +
+ Vigencia: Esta cotización es válida hasta el ${new Date(validityDate).toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric' })}. +
+ +

Si tiene alguna pregunta o desea proceder con el pedido, no dude en contactarnos.

+ + +
+ + + `.trim(); + } + async confirm(id: string, tenantId: string, userId: string): Promise<{ quotation: Quotation; orderId: string }> { const quotation = await this.findById(id, tenantId); diff --git a/backend/src/shared/middleware/auth.middleware.ts b/backend/src/shared/middleware/auth.middleware.ts index a502890..d3b2d97 100644 --- a/backend/src/shared/middleware/auth.middleware.ts +++ b/backend/src/shared/middleware/auth.middleware.ts @@ -3,6 +3,8 @@ import jwt from 'jsonwebtoken'; import { config } from '../../config/index.js'; import { AuthenticatedRequest, JwtPayload, UnauthorizedError, ForbiddenError } from '../types/index.js'; import { logger } from '../utils/logger.js'; +import { permissionsService } from '../../modules/roles/permissions.service.js'; +import { permissionCacheService } from '../../modules/auth/services/permission-cache.service.js'; // Re-export AuthenticatedRequest for convenience export { AuthenticatedRequest } from '../types/index.js'; @@ -73,15 +75,63 @@ export function requirePermission(resource: string, action: string) { throw new UnauthorizedError('Usuario no autenticado'); } + const { userId, tenantId, roles } = req.user; + // Superusers bypass permission checks - if (req.user.roles.includes('super_admin')) { + if (roles.includes('super_admin')) { return next(); } - // TODO: Check permission in database - // For now, we'll implement this when we have the permission checking service - logger.debug('Permission check', { - userId: req.user.userId, + // Build permission key for cache lookup + const permissionKey = `${resource}:${action}`; + + // Try cache first for fast permission check + const cachedHasPermission = await permissionCacheService.hasPermission(userId, permissionKey); + + // If cache hit and user has permission, allow access + if (cachedHasPermission) { + logger.debug('Permission granted (cache hit)', { + userId, + resource, + action, + }); + return next(); + } + + // Cache miss or no permission in cache - check database + const hasPermission = await permissionsService.hasPermission( + tenantId, + userId, + resource, + action + ); + + if (!hasPermission) { + logger.warn('Access denied - insufficient permissions', { + userId, + tenantId, + resource, + action, + }); + throw new ForbiddenError(`No tiene permisos para ${action} en ${resource}`); + } + + // Permission granted - update cache with user's effective permissions + // This ensures future checks are faster + try { + const effectivePermissions = await permissionsService.getEffectivePermissions(tenantId, userId); + const permissionKeys = effectivePermissions.map(p => `${p.resource}:${p.action}`); + await permissionCacheService.setUserPermissions(userId, permissionKeys); + } catch (cacheError) { + // Don't fail the request if cache update fails + logger.warn('Failed to update permission cache', { + userId, + error: (cacheError as Error).message, + }); + } + + logger.debug('Permission granted (database check)', { + userId, resource, action, }); diff --git a/backend/src/shared/services/index.ts b/backend/src/shared/services/index.ts index d8834be..ad4a071 100644 --- a/backend/src/shared/services/index.ts +++ b/backend/src/shared/services/index.ts @@ -6,3 +6,9 @@ export { BaseServiceConfig, } from './base.service.js'; export { emailService, EmailOptions, EmailResult } from './email.service.js'; +export { + cacheService, + cacheKeys, + cacheTTL, + CacheOptions, +} from './cache.service.js'; diff --git a/docs/00-vision-general/ARQUITECTURA-IA.md b/docs/00-vision-general/ARQUITECTURA-IA.md new file mode 100644 index 0000000..f025c08 --- /dev/null +++ b/docs/00-vision-general/ARQUITECTURA-IA.md @@ -0,0 +1,793 @@ +--- +id: ARQUITECTURA-IA-ERP-CORE +title: Arquitectura IA - ERP Core +type: Architecture +status: Published +version: 1.0.0 +created_date: 2026-01-10 +updated_date: 2026-01-10 +--- + +# Arquitectura IA - ERP Core + +> Detalle de la arquitectura de inteligencia artificial + +## Resumen + +ERP Core implementa una arquitectura de IA que permite integracion con multiples modelos de lenguaje (LLM), herramientas de negocio via MCP, y comunicacion inteligente a traves de WhatsApp. + +--- + +## 1. Diagrama de Arquitectura + +```mermaid +graph TB + subgraph "Clientes" + WA[WhatsApp] + WEB[Web Chat] + MOB[Mobile App] + API[API REST] + end + + subgraph "Capa de Orquestacion" + WAS[WhatsApp Service] + MCP[MCP Server] + end + + subgraph "Gateway LLM" + OR[OpenRouter] + subgraph "Modelos" + CL[Claude 3] + GPT[GPT-4] + GEM[Gemini] + MIS[Mistral] + end + end + + subgraph "Herramientas MCP" + T1[Products Tools] + T2[Inventory Tools] + T3[Orders Tools] + T4[Customers Tools] + T5[Fiados Tools] + end + + subgraph "Backend" + BE[Express API] + PRED[Prediction Service] + end + + subgraph "Database" + PG[(PostgreSQL)] + MSG[messaging schema] + AI[ai schema] + end + + WA --> WAS + WEB --> MCP + MOB --> MCP + API --> MCP + + WAS --> MCP + MCP --> OR + OR --> CL + OR --> GPT + OR --> GEM + OR --> MIS + + MCP --> T1 + MCP --> T2 + MCP --> T3 + MCP --> T4 + MCP --> T5 + + T1 --> BE + T2 --> BE + T3 --> BE + T4 --> BE + T5 --> BE + + BE --> PG + WAS --> MSG + MCP --> AI + PRED --> PG +``` + +--- + +## 2. MCP Server (Model Context Protocol) + +### 2.1 Concepto + +El MCP Server implementa el protocolo Model Context Protocol de Anthropic, que permite exponer herramientas (tools) de negocio a los modelos de lenguaje de manera estandarizada. + +### 2.2 Herramientas Disponibles + +#### 2.2.1 Products Tools + +```typescript +// Herramientas de productos +const productTools = { + list_products: { + description: 'Lista productos filtrados por categoria, nombre o precio', + parameters: { + category?: string, + search?: string, + min_price?: number, + max_price?: number, + limit?: number + }, + returns: 'Array de productos con id, nombre, precio, stock' + }, + + get_product_details: { + description: 'Obtiene detalles completos de un producto', + parameters: { + product_id: string + }, + returns: 'Producto con todos sus atributos, variantes y precios' + }, + + check_product_availability: { + description: 'Verifica si hay stock suficiente de un producto', + parameters: { + product_id: string, + quantity: number + }, + returns: '{ available: boolean, current_stock: number }' + } +}; +``` + +#### 2.2.2 Inventory Tools + +```typescript +const inventoryTools = { + check_stock: { + description: 'Consulta el stock actual de productos', + parameters: { + product_ids?: string[], + warehouse_id?: string + }, + returns: 'Array de { product_id, quantity, location }' + }, + + get_low_stock_products: { + description: 'Lista productos que estan por debajo del minimo', + parameters: { + threshold?: number + }, + returns: 'Array de productos con stock bajo' + }, + + record_inventory_movement: { + description: 'Registra un movimiento de inventario', + parameters: { + product_id: string, + quantity: number, + movement_type: 'in' | 'out' | 'adjustment', + reason?: string + }, + returns: 'Movimiento registrado' + }, + + get_inventory_value: { + description: 'Calcula el valor total del inventario', + parameters: { + warehouse_id?: string + }, + returns: '{ total_value: number, items_count: number }' + } +}; +``` + +#### 2.2.3 Orders Tools + +```typescript +const orderTools = { + create_order: { + description: 'Crea un nuevo pedido', + parameters: { + customer_id: string, + items: Array<{ product_id: string, quantity: number }>, + payment_method?: string, + notes?: string + }, + returns: 'Pedido creado con id y total' + }, + + get_order_status: { + description: 'Consulta el estado de un pedido', + parameters: { + order_id: string + }, + returns: 'Estado del pedido y detalles' + }, + + update_order_status: { + description: 'Actualiza el estado de un pedido', + parameters: { + order_id: string, + status: 'pending' | 'confirmed' | 'preparing' | 'ready' | 'delivered' | 'cancelled' + }, + returns: 'Pedido actualizado' + } +}; +``` + +#### 2.2.4 Customers Tools + +```typescript +const customerTools = { + search_customers: { + description: 'Busca clientes por nombre, telefono o email', + parameters: { + query: string, + limit?: number + }, + returns: 'Array de clientes encontrados' + }, + + get_customer_balance: { + description: 'Obtiene el saldo actual de un cliente', + parameters: { + customer_id: string + }, + returns: '{ balance: number, credit_limit: number }' + } +}; +``` + +#### 2.2.5 Fiados Tools (Credito) + +```typescript +const fiadoTools = { + get_fiado_balance: { + description: 'Consulta el saldo de credito de un cliente', + parameters: { + customer_id: string + }, + returns: '{ balance: number, available_credit: number }' + }, + + create_fiado: { + description: 'Registra una venta a credito', + parameters: { + customer_id: string, + amount: number, + order_id?: string, + description?: string + }, + returns: 'Registro de fiado creado' + }, + + register_fiado_payment: { + description: 'Registra un abono a la cuenta de credito', + parameters: { + customer_id: string, + amount: number, + payment_method?: string + }, + returns: 'Pago registrado y nuevo saldo' + }, + + check_fiado_eligibility: { + description: 'Verifica si un cliente puede comprar a credito', + parameters: { + customer_id: string, + amount: number + }, + returns: '{ eligible: boolean, reason?: string }' + } +}; +``` + +### 2.3 Recursos MCP + +```typescript +const mcpResources = { + 'erp://config/business': { + description: 'Configuracion del negocio', + returns: '{ name, address, phone, hours, policies }' + }, + + 'erp://catalog/categories': { + description: 'Categorias de productos', + returns: 'Array de categorias' + }, + + 'erp://inventory/summary': { + description: 'Resumen de inventario', + returns: '{ total_products, total_value, low_stock_count }' + } +}; +``` + +--- + +## 3. Gateway LLM (OpenRouter) + +### 3.1 Arquitectura + +```mermaid +graph LR + APP[Aplicacion] --> OR[OpenRouter API] + OR --> |"Route"| M1[Claude 3 Haiku] + OR --> |"Route"| M2[Claude 3 Sonnet] + OR --> |"Route"| M3[GPT-4o-mini] + OR --> |"Route"| M4[GPT-3.5 Turbo] + OR --> |"Route"| M5[Mistral 7B] + OR --> |"Route"| M6[Llama 3] +``` + +### 3.2 Modelos Soportados + +| Modelo | ID | Input/1M | Output/1M | Uso Recomendado | +|--------|----|---------:|----------:|-----------------| +| Claude 3 Haiku | anthropic/claude-3-haiku | $0.25 | $1.25 | Default (rapido) | +| Claude 3 Sonnet | anthropic/claude-3-sonnet | $3.00 | $15.00 | Premium | +| GPT-4o-mini | openai/gpt-4o-mini | $0.15 | $0.60 | Fallback economico | +| GPT-3.5 Turbo | openai/gpt-3.5-turbo | $0.50 | $1.50 | Fallback | +| Mistral 7B | mistralai/mistral-7b | $0.06 | $0.06 | Ultra-economico | +| Llama 3 | meta-llama/llama-3-8b | $0.20 | $0.20 | Open source | + +### 3.3 Configuracion por Tenant + +```typescript +interface TenantLLMConfig { + provider: 'openrouter' | 'openai' | 'anthropic' | 'ollama'; + api_key: string; // encriptada + model: string; + max_tokens: number; + temperature: number; + system_prompt: string; +} + +// Ejemplo de configuracion +const config: TenantLLMConfig = { + provider: 'openrouter', + api_key: 'sk-or-v1-...', + model: 'anthropic/claude-3-haiku', + max_tokens: 1000, + temperature: 0.7, + system_prompt: 'Eres un asistente de ventas amigable...' +}; +``` + +### 3.4 Estrategia de Fallback + +```typescript +async function callLLM(messages: Message[]): Promise { + const models = [ + config.model, // Modelo preferido del tenant + 'anthropic/claude-3-haiku', // Fallback 1 + 'openai/gpt-3.5-turbo' // Fallback 2 + ]; + + for (const model of models) { + try { + const response = await openrouter.chat({ + model, + messages, + max_tokens: config.max_tokens, + temperature: config.temperature + }); + return response; + } catch (error) { + if (error.code === 'rate_limit_exceeded') { + continue; // Intentar siguiente modelo + } + throw error; + } + } + + // Si todos fallan, respuesta predefinida + return { content: 'Lo siento, no puedo procesar tu solicitud ahora.' }; +} +``` + +### 3.5 Tracking de Tokens + +```sql +-- Schema: ai + +CREATE TABLE ai.configs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id), + provider VARCHAR(20) NOT NULL, + model VARCHAR(100) NOT NULL, + temperature DECIMAL(3,2) DEFAULT 0.7, + max_tokens INTEGER DEFAULT 1000, + system_prompt TEXT, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE ai.usage ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL, + model VARCHAR(100) NOT NULL, + operation VARCHAR(50), -- chat, completion, embedding + input_tokens INTEGER NOT NULL, + output_tokens INTEGER NOT NULL, + cost_usd DECIMAL(10,6), + latency_ms INTEGER, + success BOOLEAN DEFAULT true, + error_message TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Indice para reportes de uso +CREATE INDEX idx_ai_usage_tenant_date + ON ai.usage(tenant_id, created_at DESC); +``` + +--- + +## 4. WhatsApp Service con IA + +### 4.1 Flujo de Mensaje + +```mermaid +sequenceDiagram + participant C as Cliente + participant WA as WhatsApp + participant META as Meta API + participant WS as WhatsApp Service + participant LLM as LLM Service + participant MCP as MCP Server + participant BE as Backend + + C->>WA: Envia mensaje + WA->>META: Webhook + META->>WS: POST /webhook + WS->>WS: Verificar firma + WS->>WS: Extraer tenant + WS->>LLM: Procesar mensaje + LLM->>LLM: Cargar contexto + LLM->>MCP: Llamar con tools + + alt Necesita datos + MCP->>BE: Ejecutar tool + BE-->>MCP: Resultado + end + + MCP-->>LLM: Respuesta con datos + LLM-->>WS: Texto de respuesta + WS->>META: Enviar mensaje + META->>WA: Entrega + WA->>C: Muestra mensaje +``` + +### 4.2 Tipos de Mensaje Soportados + +#### Entrantes + +| Tipo | Descripcion | Procesamiento | +|------|-------------|---------------| +| text | Texto simple | Directo a LLM | +| audio | Nota de voz | Whisper -> LLM | +| image | Imagen | Vision/OCR -> LLM | +| location | Ubicacion | Extraer coords -> LLM | +| interactive | Respuesta de botones | Mapear a accion | + +#### Salientes + +| Tipo | Uso | +|------|-----| +| text | Respuestas de texto | +| template | Templates pre-aprobados | +| interactive | Botones o listas | +| media | Imagenes, documentos | + +### 4.3 Contexto de Conversacion + +```typescript +interface ConversationContext { + tenant_id: string; + customer_id?: string; + customer_name: string; + phone_number: string; + history: Message[]; // Ultimos 20 mensajes + pending_action?: string; + cart?: CartItem[]; +} + +// Sistema mantiene contexto por 30 minutos de inactividad +``` + +### 4.4 System Prompts + +#### Para Clientes + +``` +Eres el asistente virtual de {{BUSINESS_NAME}}, +una tienda de barrio en Mexico. + +Ayudas a los clientes con: +- Informacion sobre productos y precios +- Hacer pedidos +- Consultar su cuenta de fiado +- Estado de sus pedidos + +Reglas: +1. Responde en espanol mexicano casual y amigable +2. Se breve pero calido +3. Nunca inventes precios, usa las herramientas +4. Para fiados, siempre verifica primero el saldo +5. Se proactivo sugiriendo opciones +``` + +#### Para Duenos + +``` +Eres el asistente de negocios de {{BUSINESS_NAME}}. + +Ayudas al dueno con: +- Analisis de ventas y tendencias +- Gestion de inventario +- Recordatorios de cobranza +- Sugerencias de negocio + +Se directo, profesional y accionable. +Proporciona numeros concretos siempre. +Sugiere acciones si detectas problemas. +``` + +--- + +## 5. Prediccion de Inventario + +### 5.1 Algoritmo de Demanda + +```typescript +// Promedio movil ponderado (4 semanas) +function predictDemand(salesHistory: WeeklySales[]): number { + const weights = [0.40, 0.30, 0.20, 0.10]; + + // Ultimas 4 semanas + const recentWeeks = salesHistory.slice(-4).reverse(); + + let weightedSum = 0; + for (let i = 0; i < weights.length; i++) { + weightedSum += (recentWeeks[i]?.quantity ?? 0) * weights[i]; + } + + return Math.ceil(weightedSum); +} +``` + +### 5.2 Punto de Reorden + +```typescript +function calculateReorderPoint( + dailyDemand: number, + leadTimeDays: number = 3, + safetyStockDays: number = 2 +): number { + const safetyStock = dailyDemand * safetyStockDays; + const reorderPoint = (dailyDemand * leadTimeDays) + safetyStock; + return Math.ceil(reorderPoint); +} + +// Ejemplo: +// dailyDemand = 10 unidades/dia +// leadTime = 3 dias +// safetyStock = 10 * 2 = 20 unidades +// reorderPoint = (10 * 3) + 20 = 50 unidades +``` + +### 5.3 Dias de Inventario + +```typescript +function daysOfInventory(currentStock: number, dailyDemand: number): number { + if (dailyDemand === 0) return Infinity; + return Math.floor(currentStock / dailyDemand); +} + +// Alertas +// < 3 dias: CRITICO +// < 7 dias: BAJO +// > 30 dias: EXCESO +``` + +### 5.4 Endpoints de Prediccion + +```typescript +// GET /inventory/predictions +{ + products: [ + { + product_id: 'uuid', + name: 'Coca-Cola 600ml', + current_stock: 24, + predicted_weekly_demand: 50, + reorder_point: 30, + days_of_inventory: 3.4, + suggested_order: 50, + urgency: 'HIGH' + } + ] +} + +// GET /inventory/low-stock +{ + products: [ + { + product_id: 'uuid', + name: 'Coca-Cola 600ml', + current_stock: 5, + minimum_stock: 20, + shortage: 15 + } + ] +} + +// GET /inventory/slow-moving +{ + products: [ + { + product_id: 'uuid', + name: 'Cafe Premium', + current_stock: 10, + days_without_sale: 45, + last_sale_date: '2025-11-25' + } + ] +} +``` + +--- + +## 6. Modelo de Datos IA + +### 6.1 Schema: messaging + +```sql +CREATE TABLE messaging.conversations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL, + phone_number VARCHAR(20) NOT NULL, + contact_name VARCHAR(100), + conversation_type VARCHAR(20) DEFAULT 'general', + status VARCHAR(20) DEFAULT 'active', + last_message_at TIMESTAMPTZ, + last_message_preview TEXT, + unread_count INTEGER DEFAULT 0, + wa_conversation_id VARCHAR(100), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE messaging.messages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + conversation_id UUID NOT NULL REFERENCES messaging.conversations(id), + direction VARCHAR(10) NOT NULL, -- in, out + message_type VARCHAR(20) NOT NULL, -- text, audio, image, template + content TEXT, + media_url TEXT, + media_mime_type VARCHAR(50), + + -- IA Processing + processed_by_llm BOOLEAN DEFAULT false, + llm_model VARCHAR(100), + tokens_used INTEGER, + tool_calls JSONB, + + -- WhatsApp + wa_message_id VARCHAR(100), + wa_status VARCHAR(20), -- sent, delivered, read, failed + wa_timestamp TIMESTAMPTZ, + + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### 6.2 Indices + +```sql +-- Busqueda rapida de conversaciones +CREATE INDEX idx_conversations_tenant_phone + ON messaging.conversations(tenant_id, phone_number); + +CREATE INDEX idx_conversations_last_message + ON messaging.conversations(tenant_id, last_message_at DESC); + +-- Busqueda de mensajes +CREATE INDEX idx_messages_conversation + ON messaging.messages(conversation_id, created_at DESC); +``` + +--- + +## 7. Patrones de Extension IA + +### 7.1 Agregar Herramientas MCP + +```typescript +// En vertical: erp-construccion + +// Registrar herramientas adicionales +mcpServer.registerTool('construction.get_budget', { + description: 'Obtiene presupuesto de construccion', + parameters: { + budget_id: { type: 'string', required: true } + }, + handler: async ({ budget_id }) => { + return constructionService.getBudget(budget_id); + } +}); + +mcpServer.registerTool('construction.estimate_materials', { + description: 'Estima materiales para un proyecto', + parameters: { + area_m2: { type: 'number', required: true }, + project_type: { type: 'string', required: true } + }, + handler: async ({ area_m2, project_type }) => { + return constructionService.estimateMaterials(area_m2, project_type); + } +}); +``` + +### 7.2 System Prompts por Vertical + +```typescript +// Configuracion por vertical +const systemPrompts = { + 'erp-core': 'Eres un asistente de negocios general...', + 'erp-construccion': 'Eres un asistente especializado en construccion...', + 'erp-retail': 'Eres un asistente de punto de venta...' +}; +``` + +### 7.3 Modelos por Caso de Uso + +```typescript +// Diferentes modelos para diferentes tareas +const modelConfig = { + chat_simple: 'anthropic/claude-3-haiku', // Rapido y economico + chat_complex: 'anthropic/claude-3-sonnet', // Alta calidad + analysis: 'openai/gpt-4o', // Analisis profundo + embedding: 'openai/text-embedding-3-small' // Embeddings +}; +``` + +--- + +## 8. Seguridad IA + +### 8.1 Checklist + +- [ ] API keys encriptadas en base de datos +- [ ] Rate limiting por tenant +- [ ] Limite de tokens por request +- [ ] Logs de todas las llamadas LLM +- [ ] Sanitizacion de inputs +- [ ] No exponer datos sensibles a LLM + +### 8.2 Rate Limiting + +```typescript +// Limites por plan +const rateLimits = { + free: { tokensPerDay: 1000, requestsPerMinute: 5 }, + starter: { tokensPerDay: 10000, requestsPerMinute: 20 }, + pro: { tokensPerDay: 100000, requestsPerMinute: 60 }, + enterprise: { tokensPerDay: -1, requestsPerMinute: 200 } +}; +``` + +--- + +## Referencias + +- [VISION-ERP-CORE.md](VISION-ERP-CORE.md) - Vision general +- [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md) - Integraciones +- [Anthropic MCP](https://github.com/anthropics/anthropic-cookbook) - Documentacion MCP +- [OpenRouter](https://openrouter.ai/docs) - Documentacion API + +--- + +*Actualizado: 2026-01-10* diff --git a/docs/00-vision-general/ARQUITECTURA-SAAS.md b/docs/00-vision-general/ARQUITECTURA-SAAS.md new file mode 100644 index 0000000..ee94585 --- /dev/null +++ b/docs/00-vision-general/ARQUITECTURA-SAAS.md @@ -0,0 +1,558 @@ +--- +id: ARQUITECTURA-SAAS-ERP-CORE +title: Arquitectura SaaS - ERP Core +type: Architecture +status: Published +version: 1.0.0 +created_date: 2026-01-10 +updated_date: 2026-01-10 +--- + +# Arquitectura SaaS - ERP Core + +> Detalle de la arquitectura de plataforma SaaS multi-tenant + +## Resumen + +ERP Core implementa una arquitectura SaaS completa que permite a multiples organizaciones (tenants) usar la misma instancia de la aplicacion con aislamiento total de datos. + +--- + +## 1. Diagrama de Arquitectura + +```mermaid +graph TB + subgraph "Clientes" + U1[Usuario Tenant A] + U2[Usuario Tenant B] + U3[SuperAdmin] + end + + subgraph "Frontend" + FE[React App] + PA[Portal Admin] + PSA[Portal SuperAdmin] + end + + subgraph "API Gateway" + AG[Express.js] + MW1[Auth Middleware] + MW2[Tenant Middleware] + MW3[Rate Limiter] + end + + subgraph "Servicios Backend" + AUTH[Auth Service] + USER[User Service] + BILL[Billing Service] + PLAN[Plans Service] + NOTIF[Notification Service] + WH[Webhook Service] + FF[Feature Flags] + end + + subgraph "Base de Datos" + PG[(PostgreSQL)] + RLS[Row-Level Security] + end + + subgraph "Servicios Externos" + STRIPE[Stripe] + SG[SendGrid] + REDIS[Redis] + end + + U1 --> FE + U2 --> FE + U3 --> PSA + + FE --> AG + PA --> AG + PSA --> AG + + AG --> MW1 --> MW2 --> MW3 + + MW3 --> AUTH + MW3 --> USER + MW3 --> BILL + MW3 --> PLAN + MW3 --> NOTIF + MW3 --> WH + MW3 --> FF + + AUTH --> PG + USER --> PG + BILL --> PG + PLAN --> PG + NOTIF --> PG + WH --> PG + FF --> PG + + PG --> RLS + + BILL --> STRIPE + NOTIF --> SG + WH --> REDIS +``` + +--- + +## 2. Multi-Tenancy con Row-Level Security (RLS) + +### 2.1 Concepto + +Row-Level Security (RLS) es una caracteristica de PostgreSQL que permite filtrar automaticamente las filas de una tabla basandose en politicas definidas. Esto garantiza aislamiento de datos entre tenants a nivel de base de datos. + +### 2.2 Implementacion + +#### 2.2.1 Estructura de Tabla Multi-Tenant + +```sql +CREATE TABLE tenants.tenants ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(100) NOT NULL, + slug VARCHAR(50) UNIQUE NOT NULL, + settings JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Tabla de ejemplo con tenant_id +CREATE TABLE users.users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id), + email VARCHAR(255) NOT NULL, + name VARCHAR(100), + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(tenant_id, email) +); +``` + +#### 2.2.2 Politicas RLS + +```sql +-- Habilitar RLS en la tabla +ALTER TABLE users.users ENABLE ROW LEVEL SECURITY; + +-- Politica de SELECT +CREATE POLICY users_tenant_isolation_select + ON users.users FOR SELECT + USING (tenant_id = current_setting('app.current_tenant_id')::UUID); + +-- Politica de INSERT +CREATE POLICY users_tenant_isolation_insert + ON users.users FOR INSERT + WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::UUID); + +-- Politica de UPDATE +CREATE POLICY users_tenant_isolation_update + ON users.users FOR UPDATE + USING (tenant_id = current_setting('app.current_tenant_id')::UUID); + +-- Politica de DELETE +CREATE POLICY users_tenant_isolation_delete + ON users.users FOR DELETE + USING (tenant_id = current_setting('app.current_tenant_id')::UUID); +``` + +#### 2.2.3 Middleware de Contexto + +```typescript +// tenant.middleware.ts +export async function tenantMiddleware(req, res, next) { + const tenantId = req.user?.tenantId; + + if (!tenantId) { + return res.status(401).json({ error: 'Tenant not found' }); + } + + // Establecer contexto de tenant en PostgreSQL + await db.query(`SET app.current_tenant_id = '${tenantId}'`); + + next(); +} +``` + +### 2.3 Ventajas de RLS + +| Ventaja | Descripcion | +|---------|-------------| +| Seguridad | Aislamiento a nivel de base de datos | +| Simplicidad | Una sola base de datos para todos los tenants | +| Performance | Indices compartidos, optimizacion global | +| Migraciones | Una migracion aplica a todos los tenants | +| Escalabilidad | Puede manejar millones de tenants | + +--- + +## 3. Billing y Suscripciones + +### 3.1 Diagrama de Flujo + +```mermaid +sequenceDiagram + participant U as Usuario + participant FE as Frontend + participant BE as Backend + participant S as Stripe + participant DB as Database + + U->>FE: Selecciona plan + FE->>BE: POST /billing/checkout + BE->>S: Crear Checkout Session + S-->>BE: Session URL + BE-->>FE: Redirect URL + FE->>S: Redirect a Stripe + U->>S: Completa pago + S->>BE: Webhook: checkout.session.completed + BE->>DB: Crear suscripcion + BE->>S: Obtener detalles + S-->>BE: Subscription details + BE->>DB: Actualizar tenant + BE-->>FE: Success +``` + +### 3.2 Estados de Suscripcion + +```mermaid +stateDiagram-v2 + [*] --> trialing: Registro + trialing --> active: Pago exitoso + trialing --> cancelled: No paga + active --> past_due: Pago fallido + past_due --> active: Pago recuperado + past_due --> cancelled: Sin pago 30 dias + active --> cancelled: Cancelacion + cancelled --> [*] +``` + +### 3.3 Modelo de Datos + +```sql +-- Schema: billing + +CREATE TABLE billing.subscriptions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id), + stripe_subscription_id VARCHAR(100) UNIQUE, + stripe_customer_id VARCHAR(100), + plan_id UUID REFERENCES plans.plans(id), + status VARCHAR(20) NOT NULL, -- trialing, active, past_due, cancelled + current_period_start TIMESTAMPTZ, + current_period_end TIMESTAMPTZ, + trial_end TIMESTAMPTZ, + cancel_at_period_end BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE billing.invoices ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL, + subscription_id UUID REFERENCES billing.subscriptions(id), + stripe_invoice_id VARCHAR(100) UNIQUE, + amount_due INTEGER NOT NULL, -- en centavos + amount_paid INTEGER DEFAULT 0, + currency VARCHAR(3) DEFAULT 'USD', + status VARCHAR(20), -- draft, open, paid, void, uncollectible + invoice_url TEXT, + invoice_pdf TEXT, + due_date TIMESTAMPTZ, + paid_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### 3.4 Webhooks de Stripe + +| Evento | Accion | +|--------|--------| +| `customer.subscription.created` | Crear registro de suscripcion | +| `customer.subscription.updated` | Actualizar plan/status | +| `customer.subscription.deleted` | Marcar como cancelado | +| `invoice.paid` | Registrar pago exitoso | +| `invoice.payment_failed` | Notificar fallo, marcar past_due | + +--- + +## 4. Planes y Feature Gating + +### 4.1 Modelo de Planes + +```sql +-- Schema: plans + +CREATE TABLE plans.plans ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(50) NOT NULL, + slug VARCHAR(50) UNIQUE NOT NULL, + stripe_price_id VARCHAR(100), + price_monthly INTEGER NOT NULL, -- en centavos + price_yearly INTEGER, + currency VARCHAR(3) DEFAULT 'USD', + trial_days INTEGER DEFAULT 14, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE plans.plan_features ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + plan_id UUID NOT NULL REFERENCES plans.plans(id), + feature_key VARCHAR(50) NOT NULL, -- ej: 'ai_assistant' + feature_value JSONB NOT NULL, -- true/false o {limit: 100} + UNIQUE(plan_id, feature_key) +); +``` + +### 4.2 Planes Propuestos + +| Plan | Precio/mes | Usuarios | Storage | AI | Webhooks | +|------|-----------|----------|---------|-----|----------| +| Free | $0 | 1 | 100MB | No | No | +| Starter | $29 | 5 | 1GB | No | No | +| Pro | $79 | 20 | 10GB | Si | Si | +| Enterprise | $199 | Unlimited | Unlimited | Si | Si | + +### 4.3 Feature Gating + +```typescript +// plans.service.ts + +// Verificar si tenant tiene feature +async hasFeature(tenantId: string, feature: string): Promise { + const subscription = await this.getActiveSubscription(tenantId); + const planFeature = await this.getPlanFeature(subscription.planId, feature); + return planFeature?.feature_value === true; +} + +// Verificar limite numerico +async checkLimit(tenantId: string, limitKey: string, currentCount: number): Promise { + const subscription = await this.getActiveSubscription(tenantId); + const planFeature = await this.getPlanFeature(subscription.planId, limitKey); + const limit = planFeature?.feature_value?.limit ?? 0; + return limit === -1 || currentCount < limit; // -1 = unlimited +} +``` + +### 4.4 Uso en Controllers + +```typescript +// users.controller.ts + +@Post() +@RequiresFeature('users.create') +@CheckLimit('users') +async createUser(@Body() dto: CreateUserDto) { + // Solo se ejecuta si tiene la feature y no excede el limite +} +``` + +--- + +## 5. Webhooks Outbound + +### 5.1 Diagrama de Flujo + +```mermaid +sequenceDiagram + participant App as Aplicacion + participant WS as Webhook Service + participant Q as Redis Queue + participant W as Worker + participant EP as Endpoint Externo + + App->>WS: Evento ocurrio + WS->>Q: Encolar trabajo + Q-->>W: Procesar + W->>EP: POST con payload firmado + + alt Exito (2xx) + EP-->>W: 200 OK + W->>DB: Marcar entregado + else Fallo + EP-->>W: Error + W->>Q: Re-encolar (retry) + end +``` + +### 5.2 Firma HMAC + +```typescript +// webhook.service.ts + +function signPayload(payload: string, secret: string, timestamp: number): string { + const signatureInput = `${timestamp}.${payload}`; + const signature = crypto + .createHmac('sha256', secret) + .update(signatureInput) + .digest('hex'); + return `t=${timestamp},v1=${signature}`; +} + +// Header enviado +// X-Webhook-Signature: t=1704067200000,v1=abc123... +``` + +### 5.3 Politica de Reintentos + +| Intento | Delay | +|---------|-------| +| 1 | Inmediato | +| 2 | +1 minuto | +| 3 | +5 minutos | +| 4 | +30 minutos | +| 5 | +2 horas | +| 6 | +6 horas | +| Fallo | Marcar como fallido | + +### 5.4 Eventos Disponibles + +| Evento | Descripcion | +|--------|-------------| +| `user.created` | Usuario creado | +| `user.updated` | Usuario actualizado | +| `user.deleted` | Usuario eliminado | +| `subscription.created` | Suscripcion creada | +| `subscription.updated` | Suscripcion actualizada | +| `subscription.cancelled` | Suscripcion cancelada | +| `invoice.paid` | Factura pagada | +| `invoice.failed` | Pago fallido | + +--- + +## 6. Feature Flags + +### 6.1 Modelo de Datos + +```sql +-- Schema: feature_flags + +CREATE TABLE feature_flags.flags ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + key VARCHAR(50) UNIQUE NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + default_value BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE feature_flags.tenant_flags ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id), + flag_id UUID NOT NULL REFERENCES feature_flags.flags(id), + value BOOLEAN NOT NULL, + UNIQUE(tenant_id, flag_id) +); +``` + +### 6.2 Evaluacion de Flags + +```typescript +// feature-flags.service.ts + +async isEnabled(tenantId: string, flagKey: string): Promise { + // 1. Buscar override de tenant + const tenantFlag = await this.getTenantFlag(tenantId, flagKey); + if (tenantFlag !== null) return tenantFlag.value; + + // 2. Buscar valor default del flag + const flag = await this.getFlag(flagKey); + if (flag) return flag.default_value; + + // 3. Flag no existe + return false; +} +``` + +--- + +## 7. Patrones de Extension SaaS + +### 7.1 Extension de Billing + +Las verticales pueden extender el sistema de billing agregando: + +```typescript +// En vertical: erp-construccion + +// Agregar producto de Stripe para servicios adicionales +await billingService.addOneTimeCharge(tenantId, { + name: 'Cotizacion Premium', + amount: 9900, // $99.00 + description: 'Generacion de cotizacion con IA' +}); +``` + +### 7.2 Extension de Planes + +Las verticales pueden definir features adicionales: + +```sql +-- Feature especifica de construccion +INSERT INTO plans.plan_features (plan_id, feature_key, feature_value) +VALUES + ('pro-plan-id', 'construction.budgets', '{"limit": 100}'), + ('enterprise-plan-id', 'construction.budgets', '{"limit": -1}'); +``` + +### 7.3 Extension de Webhooks + +Las verticales pueden agregar eventos adicionales: + +```typescript +// Registrar evento personalizado +webhookService.registerEvent('construction.budget.approved', { + description: 'Presupuesto aprobado', + payload_schema: BudgetApprovedPayload +}); +``` + +--- + +## 8. Seguridad SaaS + +### 8.1 Checklist de Seguridad + +- [ ] RLS habilitado en todas las tablas multi-tenant +- [ ] Tokens JWT con expiracion corta (15 min) +- [ ] Refresh tokens con rotacion +- [ ] Rate limiting por tenant +- [ ] Webhooks firmados con HMAC +- [ ] Secrets encriptados en base de datos +- [ ] Audit log de acciones sensibles + +### 8.2 Headers de Seguridad + +```typescript +// Helmet configuration +app.use(helmet({ + contentSecurityPolicy: true, + crossOriginEmbedderPolicy: true, + crossOriginOpenerPolicy: true, + crossOriginResourcePolicy: true, + dnsPrefetchControl: true, + frameguard: true, + hidePoweredBy: true, + hsts: true, + ieNoOpen: true, + noSniff: true, + originAgentCluster: true, + permittedCrossDomainPolicies: true, + referrerPolicy: true, + xssFilter: true +})); +``` + +--- + +## Referencias + +- [VISION-ERP-CORE.md](VISION-ERP-CORE.md) - Vision general +- [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md) - Integraciones +- [PostgreSQL RLS](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) - Documentacion oficial +- [Stripe Billing](https://stripe.com/docs/billing) - Documentacion oficial + +--- + +*Actualizado: 2026-01-10* diff --git a/docs/00-vision-general/INTEGRACIONES-EXTERNAS.md b/docs/00-vision-general/INTEGRACIONES-EXTERNAS.md new file mode 100644 index 0000000..a62e77c --- /dev/null +++ b/docs/00-vision-general/INTEGRACIONES-EXTERNAS.md @@ -0,0 +1,448 @@ +--- +id: INTEGRACIONES-EXTERNAS-ERP-CORE +title: Integraciones Externas - ERP Core +type: Technical +status: Published +version: 1.0.0 +created_date: 2026-01-10 +updated_date: 2026-01-10 +--- + +# Integraciones Externas - ERP Core + +> Catalogo completo de integraciones con servicios externos + +## Resumen + +ERP Core integra multiples servicios externos para proveer funcionalidades de billing, comunicaciones, almacenamiento e inteligencia artificial. + +--- + +## 1. Catalogo de Integraciones + +| ID | Servicio | Proveedor | Modulo | Estado | +|----|----------|-----------|--------|--------| +| INT-001 | Billing | Stripe | MGN-016 | Planificado | +| INT-002 | Email | SendGrid/SES | MGN-008 | Planificado | +| INT-003 | Push | Web Push API | MGN-008 | Planificado | +| INT-004 | WhatsApp | Meta Cloud API | MGN-021 | Planificado | +| INT-005 | Storage | S3/R2/MinIO | N/A | Planificado | +| INT-006 | Cache/Queue | Redis/BullMQ | N/A | Planificado | +| INT-007 | LLM Gateway | OpenRouter | MGN-020 | Planificado | +| INT-008 | Transcripcion | OpenAI Whisper | MGN-021 | Planificado | +| INT-009 | Vision/OCR | Google Vision | MGN-021 | Planificado | + +--- + +## 2. INT-001: Stripe (Billing) + +### 2.1 Descripcion + +Stripe provee la infraestructura de pagos para suscripciones, facturacion y gestion de metodos de pago. + +### 2.2 Caracteristicas + +- Suscripciones recurrentes (mensual/anual) +- Trial periods configurables +- Upgrade/downgrade con prorateo +- Webhooks para sincronizacion +- Portal de cliente integrado +- Multiples monedas (USD, MXN) + +### 2.3 Configuracion + +```env +# Variables de entorno requeridas +STRIPE_SECRET_KEY=sk_live_... +STRIPE_PUBLISHABLE_KEY=pk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... +``` + +### 2.4 Webhooks Requeridos + +| Evento | Accion | +|--------|--------| +| customer.subscription.created | Activar suscripcion | +| customer.subscription.updated | Actualizar plan | +| customer.subscription.deleted | Cancelar suscripcion | +| invoice.paid | Registrar pago | +| invoice.payment_failed | Notificar fallo | + +### 2.5 Documentacion + +- [Stripe Docs](https://stripe.com/docs) +- [Stripe API Reference](https://stripe.com/docs/api) + +--- + +## 3. INT-002: SendGrid/SES (Email) + +### 3.1 Descripcion + +Servicios de email transaccional para notificaciones, verificaciones y comunicaciones con usuarios. + +### 3.2 Proveedores Soportados + +| Proveedor | Caso de uso | +|-----------|-------------| +| SendGrid | Principal - alto volumen | +| AWS SES | Alternativo - bajo costo | +| SMTP generico | Desarrollo/self-hosted | + +### 3.3 Configuracion SendGrid + +```env +SENDGRID_API_KEY=SG... +SENDGRID_FROM_EMAIL=noreply@example.com +SENDGRID_FROM_NAME=ERP Core +``` + +### 3.4 Configuracion AWS SES + +```env +AWS_SES_ACCESS_KEY_ID=AKIA... +AWS_SES_SECRET_ACCESS_KEY=... +AWS_SES_REGION=us-east-1 +AWS_SES_FROM_EMAIL=noreply@example.com +``` + +### 3.5 Templates de Email + +| Template | Uso | +|----------|-----| +| welcome | Bienvenida a nuevo usuario | +| verify_email | Verificacion de email | +| password_reset | Reset de password | +| invoice_paid | Factura pagada | +| trial_ending | Trial por terminar | + +### 3.6 Documentacion + +- [SendGrid Docs](https://docs.sendgrid.com/) +- [AWS SES Docs](https://docs.aws.amazon.com/ses/) + +--- + +## 4. INT-003: Web Push API (Notificaciones Push) + +### 4.1 Descripcion + +Notificaciones push para navegadores web usando el estandar Web Push con VAPID. + +### 4.2 Configuracion + +```env +VAPID_PUBLIC_KEY=BJ... +VAPID_PRIVATE_KEY=... +VAPID_SUBJECT=mailto:admin@example.com +``` + +### 4.3 Generacion de Claves VAPID + +```bash +npx web-push generate-vapid-keys +``` + +### 4.4 Documentacion + +- [Web Push Protocol](https://web.dev/push-notifications/) +- [web-push npm](https://www.npmjs.com/package/web-push) + +--- + +## 5. INT-004: Meta WhatsApp Business (Mensajeria) + +### 5.1 Descripcion + +WhatsApp Business Cloud API para comunicacion con clientes via WhatsApp, integrado con IA conversacional. + +### 5.2 Configuracion + +```env +WHATSAPP_ACCESS_TOKEN=EAA... +WHATSAPP_PHONE_NUMBER_ID=123456789 +WHATSAPP_WABA_ID=987654321 +WHATSAPP_WEBHOOK_VERIFY_TOKEN=my-verify-token +``` + +### 5.3 Webhooks + +| Evento | Accion | +|--------|--------| +| messages | Procesar mensaje entrante | +| message_status | Actualizar estado (sent/delivered/read) | + +### 5.4 Templates Pre-aprobados + +Los templates de WhatsApp deben ser aprobados por Meta antes de usarse: + +| Template | Uso | +|----------|-----| +| order_confirmation | Confirmacion de pedido | +| payment_reminder | Recordatorio de pago | +| shipping_update | Actualizacion de envio | + +### 5.5 Documentacion + +- [WhatsApp Business API](https://developers.facebook.com/docs/whatsapp/cloud-api) + +--- + +## 6. INT-005: S3/R2/MinIO (Storage) + +### 6.1 Descripcion + +Almacenamiento de archivos compatible con S3 API para documentos, imagenes y assets. + +### 6.2 Proveedores Soportados + +| Proveedor | Caso de uso | +|-----------|-------------| +| AWS S3 | Produccion - alta disponibilidad | +| Cloudflare R2 | Alternativo - sin egress fees | +| MinIO | Self-hosted / desarrollo | + +### 6.3 Configuracion AWS S3 + +```env +AWS_ACCESS_KEY_ID=AKIA... +AWS_SECRET_ACCESS_KEY=... +AWS_REGION=us-east-1 +AWS_S3_BUCKET=erp-core-files +``` + +### 6.4 Configuracion Cloudflare R2 + +```env +R2_ACCESS_KEY_ID=... +R2_SECRET_ACCESS_KEY=... +R2_ENDPOINT=https://xxx.r2.cloudflarestorage.com +R2_BUCKET=erp-core-files +``` + +### 6.5 Configuracion MinIO + +```env +MINIO_ENDPOINT=http://localhost:9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin +MINIO_BUCKET=erp-core-files +``` + +### 6.6 Documentacion + +- [AWS S3 Docs](https://docs.aws.amazon.com/s3/) +- [Cloudflare R2 Docs](https://developers.cloudflare.com/r2/) +- [MinIO Docs](https://min.io/docs/minio/linux/index.html) + +--- + +## 7. INT-006: Redis/BullMQ (Cache y Colas) + +### 7.1 Descripcion + +Redis para cache de datos y BullMQ para colas de trabajos asincronos. + +### 7.2 Casos de Uso + +| Uso | Proposito | +|-----|-----------| +| Session store | Almacenar sesiones de usuario | +| Cache | Cache de queries frecuentes | +| Rate limiting | Control de limite de requests | +| Job queue | Colas para webhooks, emails, etc. | + +### 7.3 Configuracion + +```env +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_TLS=false +``` + +### 7.4 Colas Definidas + +| Cola | Procesador | Uso | +|------|------------|-----| +| email | EmailProcessor | Envio de emails | +| webhook | WebhookProcessor | Envio de webhooks | +| notification | NotificationProcessor | Notificaciones push | +| llm | LLMProcessor | Requests a LLM | + +### 7.5 Documentacion + +- [Redis Docs](https://redis.io/docs/) +- [BullMQ Docs](https://docs.bullmq.io/) + +--- + +## 8. INT-007: OpenRouter (LLM Gateway) + +### 8.1 Descripcion + +Gateway unificado para acceder a multiples modelos de lenguaje (LLM) con una sola API key. + +### 8.2 Modelos Disponibles + +| Modelo | ID | Costo/1M tokens | Uso | +|--------|----|-----------------:|-----| +| Claude 3 Haiku | anthropic/claude-3-haiku | $0.25 | Default | +| Claude 3 Sonnet | anthropic/claude-3-sonnet | $3.00 | Premium | +| GPT-4o-mini | openai/gpt-4o-mini | $0.15 | Fallback | +| GPT-3.5 Turbo | openai/gpt-3.5-turbo | $0.50 | Fallback | +| Mistral 7B | mistralai/mistral-7b | $0.06 | Economico | +| Llama 3 | meta-llama/llama-3-8b | $0.20 | Open source | + +### 8.3 Configuracion + +```env +OPENROUTER_API_KEY=sk-or-v1-... +LLM_MODEL_DEFAULT=anthropic/claude-3-haiku +LLM_MODEL_FALLBACK=openai/gpt-3.5-turbo +LLM_MAX_TOKENS=1000 +LLM_TEMPERATURE=0.7 +LLM_BASE_URL=https://openrouter.ai/api/v1 +``` + +### 8.4 Rate Limits + +Los rate limits dependen del modelo y plan de OpenRouter. + +### 8.5 Documentacion + +- [OpenRouter Docs](https://openrouter.ai/docs) + +--- + +## 9. INT-008: OpenAI Whisper (Transcripcion) + +### 9.1 Descripcion + +Transcripcion de audio a texto para procesamiento de notas de voz en WhatsApp. + +### 9.2 Configuracion + +```env +OPENAI_API_KEY=sk-... +WHISPER_MODEL=whisper-1 +``` + +### 9.3 Formatos Soportados + +- MP3, MP4, MPEG, MPGA, M4A, WAV, WEBM +- Maximo 25 MB por archivo + +### 9.4 Documentacion + +- [Whisper API](https://platform.openai.com/docs/guides/speech-to-text) + +--- + +## 10. INT-009: Google Vision (OCR) + +### 10.1 Descripcion + +Vision por computadora para reconocimiento de texto en imagenes (OCR) y deteccion de productos. + +### 10.2 Casos de Uso + +| Caso | Descripcion | +|------|-------------| +| OCR de productos | Extraer nombre/precio de fotos | +| Codigo de barras | Detectar y decodificar barcodes | +| Etiquetas | Extraer texto de etiquetas | + +### 10.3 Configuracion + +```env +GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json +GOOGLE_VISION_PROJECT_ID=my-project +``` + +### 10.4 Documentacion + +- [Cloud Vision API](https://cloud.google.com/vision/docs) + +--- + +## 11. Variables de Entorno Consolidadas + +```env +# ========================================== +# STRIPE (Billing) +# ========================================== +STRIPE_SECRET_KEY=sk_live_... +STRIPE_PUBLISHABLE_KEY=pk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... + +# ========================================== +# EMAIL (SendGrid) +# ========================================== +SENDGRID_API_KEY=SG... +SENDGRID_FROM_EMAIL=noreply@example.com +SENDGRID_FROM_NAME=ERP Core + +# ========================================== +# PUSH NOTIFICATIONS (VAPID) +# ========================================== +VAPID_PUBLIC_KEY=BJ... +VAPID_PRIVATE_KEY=... +VAPID_SUBJECT=mailto:admin@example.com + +# ========================================== +# WHATSAPP +# ========================================== +WHATSAPP_ACCESS_TOKEN=EAA... +WHATSAPP_PHONE_NUMBER_ID=123456789 +WHATSAPP_WABA_ID=987654321 +WHATSAPP_WEBHOOK_VERIFY_TOKEN=my-verify-token + +# ========================================== +# STORAGE (S3) +# ========================================== +AWS_ACCESS_KEY_ID=AKIA... +AWS_SECRET_ACCESS_KEY=... +AWS_REGION=us-east-1 +AWS_S3_BUCKET=erp-core-files + +# ========================================== +# REDIS +# ========================================== +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# ========================================== +# LLM (OpenRouter) +# ========================================== +OPENROUTER_API_KEY=sk-or-v1-... +LLM_MODEL_DEFAULT=anthropic/claude-3-haiku +LLM_MODEL_FALLBACK=openai/gpt-3.5-turbo +LLM_MAX_TOKENS=1000 +LLM_TEMPERATURE=0.7 + +# ========================================== +# WHISPER (Transcripcion) +# ========================================== +OPENAI_API_KEY=sk-... + +# ========================================== +# GOOGLE VISION (OCR) +# ========================================== +GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json +GOOGLE_VISION_PROJECT_ID=my-project +``` + +--- + +## Referencias + +- [VISION-ERP-CORE.md](VISION-ERP-CORE.md) - Vision general +- [STACK-TECNOLOGICO.md](STACK-TECNOLOGICO.md) - Stack completo +- [ARQUITECTURA-SAAS.md](ARQUITECTURA-SAAS.md) - Arquitectura SaaS +- [ARQUITECTURA-IA.md](ARQUITECTURA-IA.md) - Arquitectura IA + +--- + +*Actualizado: 2026-01-10* diff --git a/docs/00-vision-general/STACK-TECNOLOGICO.md b/docs/00-vision-general/STACK-TECNOLOGICO.md new file mode 100644 index 0000000..e50278b --- /dev/null +++ b/docs/00-vision-general/STACK-TECNOLOGICO.md @@ -0,0 +1,314 @@ +--- +id: STACK-TECNOLOGICO-ERP-CORE +title: Stack Tecnologico - ERP Core +type: Technical +status: Published +version: 1.0.0 +created_date: 2026-01-10 +updated_date: 2026-01-10 +--- + +# Stack Tecnologico - ERP Core + +> Detalle completo del stack tecnologico utilizado en ERP Core + +## Resumen + +ERP Core utiliza un stack moderno basado en TypeScript tanto en backend como en frontend, con PostgreSQL como base de datos principal y servicios externos para funcionalidades SaaS e IA. + +--- + +## 1. Backend + +### 1.1 Core + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Node.js | 20+ LTS | Runtime de JavaScript | +| Express.js | 4.x | Framework HTTP | +| TypeScript | 5.3+ | Lenguaje tipado | +| TypeORM | 0.3.17+ | ORM para PostgreSQL | + +### 1.2 Autenticacion y Seguridad + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| jsonwebtoken | 9.x | Generacion/validacion JWT | +| bcryptjs | 2.x | Hash de passwords | +| helmet | 7.x | Headers de seguridad | +| cors | 2.x | Cross-Origin Resource Sharing | + +### 1.3 Validacion + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Zod | 3.x | Validacion de schemas | +| class-validator | 0.14+ | Validacion de DTOs | +| class-transformer | 0.5+ | Transformacion de objetos | + +### 1.4 Documentacion + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Swagger/OpenAPI | 3.x | Especificacion de API | +| swagger-ui-express | 5.x | UI de documentacion | + +### 1.5 Testing + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Jest | 29.x | Framework de testing | +| supertest | 6.x | Testing de HTTP | + +### 1.6 Utilidades + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Winston | 3.x | Logging estructurado | +| dotenv | 16.x | Variables de entorno | +| uuid | 9.x | Generacion de UUIDs | +| date-fns | 3.x | Manipulacion de fechas | + +### 1.7 SaaS y Colas + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Stripe SDK | Latest | Billing y suscripciones | +| BullMQ | 5.x | Colas de trabajos | +| ioredis | 5.x | Cliente Redis | + +### 1.8 IA y Comunicacion + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| OpenRouter Client | Latest | Gateway LLM | +| @anthropic-ai/sdk | Latest | SDK de Anthropic (MCP) | +| Socket.io | 4.x | WebSocket real-time | + +--- + +## 2. Frontend + +### 2.1 Core + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| React | 18.x | Framework UI | +| Vite | 5.x | Build tool y dev server | +| TypeScript | 5.3+ | Lenguaje tipado | + +### 2.2 State Management + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Zustand | 4.x | Estado global ligero | +| React Query | 5.x | Cache y fetching de datos | + +### 2.3 Estilos + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Tailwind CSS | 4.x | Utility-first CSS | +| clsx | 2.x | Condicionales de clases | + +### 2.4 Formularios + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| React Hook Form | 7.x | Manejo de formularios | +| @hookform/resolvers | 3.x | Validacion con Zod | +| Zod | 3.x | Schemas de validacion | + +### 2.5 Navegacion + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| React Router | 6.x | Routing SPA | + +### 2.6 UI Components + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Radix UI | Latest | Primitivos accesibles | +| Lucide React | Latest | Iconos | + +### 2.7 Utilidades + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| axios | 1.x | Cliente HTTP | +| date-fns | 3.x | Fechas | +| Socket.io-client | 4.x | WebSocket | + +### 2.8 Testing + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Vitest | 1.x | Testing unitario | +| Playwright | Latest | Testing E2E | +| Testing Library | 14.x | Testing de componentes | + +--- + +## 3. Database + +### 3.1 Motor Principal + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| PostgreSQL | 16+ | Base de datos relacional | + +### 3.2 Extensiones PostgreSQL + +| Extension | Proposito | +|-----------|-----------| +| uuid-ossp | Generacion de UUIDs | +| pg_trgm | Busqueda fuzzy/trigrams | +| pgcrypto | Encriptacion de datos | + +### 3.3 Caracteristicas Utilizadas + +| Caracteristica | Proposito | +|----------------|-----------| +| Row-Level Security (RLS) | Aislamiento multi-tenant | +| JSONB | Datos flexibles (configs, metadata) | +| Generated Columns | Columnas calculadas | +| Partial Indexes | Optimizacion de queries | + +### 3.4 Cache y Colas + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Redis | 7.x | Cache, sessions, colas | + +--- + +## 4. Servicios Externos + +### 4.1 Pagos y Billing + +| Servicio | Proposito | +|----------|-----------| +| Stripe | Suscripciones, pagos, facturas | + +### 4.2 Comunicaciones + +| Servicio | Proposito | +|----------|-----------| +| SendGrid | Email transaccional | +| AWS SES | Email (alternativo) | +| Web Push API | Notificaciones push | +| Meta WhatsApp Business | Mensajeria WhatsApp | + +### 4.3 Almacenamiento + +| Servicio | Proposito | +|----------|-----------| +| AWS S3 | Storage de archivos | +| Cloudflare R2 | Storage (alternativo) | +| MinIO | Storage self-hosted | + +### 4.4 Inteligencia Artificial + +| Servicio | Proposito | +|----------|-----------| +| OpenRouter | Gateway a 50+ modelos LLM | +| OpenAI Whisper | Transcripcion de audio | +| Google Vision | OCR y vision por computadora | + +--- + +## 5. DevOps + +### 5.1 Contenedores + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| Docker | 24+ | Contenedorizacion | +| Docker Compose | 2.x | Orquestacion local | + +### 5.2 CI/CD + +| Tecnologia | Proposito | +|------------|-----------| +| GitHub Actions | Pipeline de CI/CD | + +### 5.3 Linting y Formateo + +| Tecnologia | Version | Proposito | +|------------|---------|-----------| +| ESLint | 8.x | Linting de codigo | +| Prettier | 3.x | Formateo de codigo | + +--- + +## 6. Versiones Minimas Requeridas + +| Componente | Version Minima | +|------------|----------------| +| Node.js | 20.0.0 | +| npm | 10.0.0 | +| PostgreSQL | 15.0 | +| Redis | 7.0 | +| Docker | 24.0 | + +--- + +## 7. Setup de Desarrollo + +### 7.1 Prerrequisitos + +```bash +# Verificar versiones +node --version # >= 20.0.0 +npm --version # >= 10.0.0 +docker --version # >= 24.0.0 +``` + +### 7.2 Instalacion + +```bash +# Clonar repositorio +git clone erp-core +cd erp-core + +# Instalar dependencias +npm install + +# Copiar variables de entorno +cp .env.example .env + +# Levantar servicios con Docker +docker-compose up -d + +# Ejecutar migraciones +npm run db:migrate + +# Iniciar desarrollo +npm run dev +``` + +### 7.3 Puertos por Defecto + +| Servicio | Puerto | +|----------|--------| +| Backend API | 3000 | +| Frontend | 5173 | +| PostgreSQL | 5432 | +| Redis | 6379 | +| MCP Server | 3142 | +| WhatsApp Service | 3143 | + +--- + +## Referencias + +- [VISION-ERP-CORE.md](VISION-ERP-CORE.md) - Vision general +- [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md) - Detalle de integraciones +- [Node.js](https://nodejs.org/) - Documentacion oficial +- [React](https://react.dev/) - Documentacion oficial +- [PostgreSQL](https://www.postgresql.org/docs/) - Documentacion oficial + +--- + +*Actualizado: 2026-01-10* diff --git a/docs/00-vision-general/VISION-ERP-CORE.md b/docs/00-vision-general/VISION-ERP-CORE.md index 04abcc5..0955137 100644 --- a/docs/00-vision-general/VISION-ERP-CORE.md +++ b/docs/00-vision-general/VISION-ERP-CORE.md @@ -1,9 +1,39 @@ +--- +id: VISION-ERP-CORE +title: Vision General - ERP Core +type: Vision +status: Published +priority: P0 +version: 2.0.0 +created_date: 2025-12-01 +updated_date: 2026-01-10 +--- + # Vision General: ERP Core ## Resumen Ejecutivo ERP Core es la **base generica reutilizable** que proporciona el 60-70% del codigo compartido para todas las verticales del ERP Suite. Es una adaptacion de los patrones de Odoo al stack TypeScript/Node.js/React. +**Capacidades Core:** +- Multi-tenancy con Row-Level Security (RLS) +- Autenticacion/Autorizacion avanzada (JWT, OAuth, RBAC) +- Modulos de negocio genericos (Inventario, Ventas, Compras, CRM) + +**Capacidades SaaS (Plataforma):** +- Billing y suscripciones con Stripe +- Planes con feature gating y limites +- Notificaciones multicanal (Email, Push, WhatsApp) +- Webhooks outbound con firma HMAC +- Feature flags por tenant/usuario + +**Capacidades IA (Inteligencia):** +- Integracion LLM multi-proveedor (OpenRouter) +- MCP Server para herramientas de negocio +- WhatsApp Business con IA conversacional +- Prediccion de inventario y demanda +- Asistente de negocio inteligente + --- ## Proposito @@ -19,6 +49,11 @@ Desarrollar ERPs verticales desde cero es costoso y repetitivo. El 60-70% de la - Ventas y compras - Contabilidad basica +**Ademas, las plataformas modernas requieren:** +- Modelo de negocio SaaS con suscripciones +- Inteligencia artificial integrada +- Comunicacion omnicanal con clientes + ### Solucion ERP Core provee esta funcionalidad comun de forma: @@ -26,6 +61,8 @@ ERP Core provee esta funcionalidad comun de forma: - **Extensible:** Las verticales pueden extender sin modificar - **Multi-tenant:** Aislamiento por tenant desde el diseno - **Documentado:** Documentacion antes de desarrollo +- **SaaS-Ready:** Billing, planes y self-service incluidos +- **AI-First:** IA integrada en flujos de negocio --- @@ -35,16 +72,24 @@ ERP Core provee esta funcionalidad comun de forma: 1. Completar modulos core: Auth, Users, Roles, Tenants 2. Implementar Partners y Products 3. Establecer patrones de extension para verticales +4. **Integrar Billing y Plans (Stripe)** +5. **Implementar Feature Flags** ### Mediano Plazo (6 meses) 1. Completar Sales, Purchases, Inventory 2. Implementar Financial basico 3. Primera vertical (Construccion) usando el core +4. **Integrar Notificaciones multicanal** +5. **Implementar Webhooks outbound** +6. **Integrar MCP Server y LLM** ### Largo Plazo (12 meses) 1. Todas las verticales usando el core -2. SaaS layer para autocontratacion +2. **SaaS layer completo para autocontratacion** 3. Marketplace de extensiones +4. **WhatsApp Business con IA integrada** +5. **Prediccion de inventario y demanda** +6. **Asistente de negocio inteligente** --- @@ -53,55 +98,100 @@ ERP Core provee esta funcionalidad comun de forma: ### Modelo de Capas ``` -┌─────────────────────────────────────────────────────────────┐ -│ FRONTEND │ -│ React 18 + TypeScript + Tailwind + Zustand │ -├─────────────────────────────────────────────────────────────┤ -│ API REST │ -│ Express.js + TypeScript + Swagger │ -├─────────────────────────────────────────────────────────────┤ -│ BACKEND │ -│ Modulos: Auth | Users | Partners | Products | Sales... │ -│ Services + Controllers + DTOs + Entities │ -├─────────────────────────────────────────────────────────────┤ -│ DATABASE │ -│ PostgreSQL 15+ con RLS (Row-Level Security) │ -│ Schemas: core_auth | core_partners | core_products... │ -└─────────────────────────────────────────────────────────────┘ ++------------------------------------------------------------------+ +| CLIENTES | +| +----------+ +----------+ +----------+ +----------+ | +| | Web App | |Mobile App| | WhatsApp | | API Rest | | +| | React 18 | | Expo 51 | | Meta API | | Swagger | | +| +----------+ +----------+ +----------+ +----------+ | ++------------------------------------------------------------------+ +| CAPA IA | +| +----------------------------------------------------------+ | +| | MCP SERVER | | +| | (Model Context Protocol - Herramientas de Negocio) | | +| +----------------------------------------------------------+ | +| | LLM GATEWAY (OpenRouter) | | +| | Claude | GPT-4 | Gemini | Mistral | Llama | | +| +----------------------------------------------------------+ | ++------------------------------------------------------------------+ +| API GATEWAY | +| +----------------------------------------------------------+ | +| | Express.js + TypeScript + Swagger | | +| | Middleware: Auth | Tenant | Rate Limit | Validation | | +| +----------------------------------------------------------+ | ++------------------------------------------------------------------+ +| SERVICIOS BACKEND | +| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ | +| | Auth | | Users | |Tenants| |Billing| | Plans | |Webhooks| | +| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ | +| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ | +| |Notif | |Storage| | Audit | |FFlags | | AI | |WhatsApp| | +| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ | +| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ | +| |Catalog| |Invntry| | Sales | |Purchas| | CRM | |Projects| | +| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ | ++------------------------------------------------------------------+ +| BASE DE DATOS | +| +----------------------------------------------------------+ | +| | PostgreSQL 16+ con RLS (Row-Level Security) | | +| | Schemas: auth | users | tenants | billing | plans | ai | | +| | notifications | webhooks | storage | audit | | +| | core_catalog | core_inventory | core_sales | | +| +----------------------------------------------------------+ | ++------------------------------------------------------------------+ +| SERVICIOS EXTERNOS | +| +--------+ +--------+ +--------+ +--------+ +--------+ | +| | Stripe | |SendGrid| | S3/R2 | | Redis | |OpenRouter| | +| +--------+ +--------+ +--------+ +--------+ +--------+ | +| +--------+ +--------+ | +| |WhatsApp| | Vision | | +| +--------+ +--------+ | ++------------------------------------------------------------------+ ``` ### Modelo de Extension ``` -┌─────────────────────────────────────────────────────────────┐ -│ ERP CORE │ -│ Modulos Genericos (MGN-001 a MGN-015) │ -│ 60-70% funcionalidad comun │ -└────────────────────────┬────────────────────────────────────┘ - │ EXTIENDE - ┌────────────────┼────────────────┐ - ↓ ↓ ↓ -┌───────────────┐ ┌───────────────┐ ┌───────────────┐ -│ Construccion │ │Vidrio Templado│ │ Retail │ -│ (MAI-*) │ │ (MVT-*) │ │ (MRT-*) │ -│ 30-40% extra │ │ 30-40% extra │ │ 30-40% extra │ -└───────────────┘ └───────────────┘ └───────────────┘ ++------------------------------------------------------------------+ +| ERP CORE | +| Modulos Genericos (MGN-001 a MGN-022) | +| 60-70% funcionalidad comun | +| | +| +-- Core Business: auth, users, tenants, catalog, inventory | +| +-- SaaS Layer: billing, plans, webhooks, feature-flags | +| +-- IA Layer: ai-integration, whatsapp, mcp-server | ++------------------------------------------------------------------+ + | EXTIENDE + +----------------+----------------+ + | | | ++---------------+ +---------------+ +---------------+ +| Construccion | |Vidrio Templado| | Retail | +| (MAI-*) | | (MVT-*) | | (MRT-*) | +| 30-40% extra | | 30-40% extra | | 30-40% extra | ++---------------+ +---------------+ +---------------+ ``` --- ## Modulos Core (MGN-*) +### Fase Foundation (P0) + | Codigo | Modulo | Descripcion | Prioridad | Estado | |--------|--------|-------------|-----------|--------| | MGN-001 | auth | Autenticacion JWT, OAuth, sessions | P0 | En desarrollo | | MGN-002 | users | Gestion de usuarios CRUD | P0 | En desarrollo | | MGN-003 | roles | Roles y permisos (RBAC) | P0 | Planificado | -| MGN-004 | tenants | Multi-tenancy, aislamiento | P0 | Planificado | +| MGN-004 | tenants | Multi-tenancy, aislamiento RLS | P0 | Planificado | + +### Fase Core Business (P1) + +| Codigo | Modulo | Descripcion | Prioridad | Estado | +|--------|--------|-------------|-----------|--------| | MGN-005 | catalogs | Catalogos maestros genericos | P1 | Planificado | | MGN-006 | settings | Configuracion del sistema | P1 | Planificado | | MGN-007 | audit | Auditoria y logs | P1 | Planificado | -| MGN-008 | notifications | Sistema de notificaciones | P2 | Planificado | +| MGN-008 | notifications | Sistema de notificaciones multicanal | P1 | Planificado | | MGN-009 | reports | Reportes genericos | P2 | Planificado | | MGN-010 | financial | Contabilidad basica | P1 | Planificado | | MGN-011 | inventory | Inventario basico | P1 | Planificado | @@ -110,47 +200,174 @@ ERP Core provee esta funcionalidad comun de forma: | MGN-014 | crm | CRM basico | P2 | Planificado | | MGN-015 | projects | Proyectos genericos | P2 | Planificado | +### Fase SaaS Platform (P3) + +| Codigo | Modulo | Descripcion | Prioridad | Estado | +|--------|--------|-------------|-----------|--------| +| MGN-016 | billing | Suscripciones y pagos (Stripe) | P3 | Planificado | +| MGN-017 | plans | Planes, limites y feature gating | P3 | Planificado | +| MGN-018 | webhooks | Webhooks outbound con firma HMAC | P3 | Planificado | +| MGN-019 | feature-flags | Feature flags por tenant/usuario | P3 | Planificado | + +### Fase IA Intelligence (P3) + +| Codigo | Modulo | Descripcion | Prioridad | Estado | +|--------|--------|-------------|-----------|--------| +| MGN-020 | ai-integration | Gateway LLM multi-proveedor | P3 | Planificado | +| MGN-021 | whatsapp-business | WhatsApp Business con IA | P3 | Planificado | +| MGN-022 | mcp-server | MCP Server para herramientas | P3 | Planificado | + +--- + +## Alcance SaaS (Plataforma) + +> Ver detalles completos en [ARQUITECTURA-SAAS.md](ARQUITECTURA-SAAS.md) + +### Billing y Suscripciones (MGN-016) + +**Proveedor:** Stripe + +**Caracteristicas:** +- Suscripciones recurrentes (mensual/anual) +- Trial gratuito configurable +- Upgrade/downgrade con prorateo automatico +- Facturas y recibos +- Webhooks Stripe sincronizados +- Portal de cliente Stripe integrado + +### Planes y Limites (MGN-017) + +| Plan | Precio | Usuarios | Storage | Features | +|------|--------|----------|---------|----------| +| Free | $0/mes | 1 | 100MB | Core basico | +| Starter | $29/mes | 5 | 1GB | + API access | +| Pro | $79/mes | 20 | 10GB | + AI assistant + Webhooks | +| Enterprise | $199/mes | Unlimited | Unlimited | + Custom branding + SLA | + +### Notificaciones Multicanal (MGN-008) + +**Canales:** +- Email (SendGrid, AWS SES, SMTP) +- Push Web (Web Push API con VAPID) +- In-App (WebSocket real-time) +- WhatsApp Business (Meta Cloud API) + +### Webhooks Outbound (MGN-018) + +**Eventos:** user.*, subscription.*, invoice.*, tenant.* + +**Seguridad:** Firma HMAC-SHA256, reintentos exponenciales + +### Feature Flags (MGN-019) + +- Flags por tenant y por usuario +- Rollout gradual +- A/B testing +- Evaluaciones por contexto + +--- + +## Alcance IA (Inteligencia) + +> Ver detalles completos en [ARQUITECTURA-IA.md](ARQUITECTURA-IA.md) + +### Gateway LLM (MGN-020) + +**Gateway:** OpenRouter (acceso a 50+ modelos) + +| Modelo | Costo/1M tokens | Uso | +|--------|-----------------:|-----| +| Claude 3 Haiku | $0.25 | Default | +| Claude 3 Sonnet | $3.00 | Premium | +| GPT-4o-mini | $0.15 | Fallback | +| Mistral 7B | $0.06 | Economico | + +**Caracteristicas:** +- Cambio de modelo sin modificar codigo +- Fallback automatico entre modelos +- Token tracking por tenant +- Configuracion por tenant + +### MCP Server (MGN-022) + +**Herramientas de Negocio:** +- **Productos:** list, details, availability +- **Inventario:** stock, low-stock, movements +- **Ventas:** create_order, status, update +- **Clientes:** search, balance +- **Fiados:** balance, create, payment + +### WhatsApp Business (MGN-021) + +**Flujo:** Cliente -> WhatsApp -> LLM -> MCP -> Respuesta + +**Tipos de mensaje:** texto, audio, imagen, ubicacion + +### Prediccion de Inventario + +**Algoritmos:** +- Demanda: Promedio movil ponderado (4 semanas) +- Reorden: (Demanda_diaria x Lead_time) + Stock_seguridad +- Dias de inventario: Stock_actual / Demanda_diaria + +--- + +## Integraciones Externas + +> Ver catalogo completo en [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md) + +| Categoria | Servicio | Modulo | Uso | +|-----------|----------|--------|-----| +| Pagos | Stripe | MGN-016 | Billing y suscripciones | +| Email | SendGrid/SES | MGN-008 | Notificaciones | +| Push | Web Push API | MGN-008 | Notificaciones | +| WhatsApp | Meta Cloud API | MGN-021 | Mensajeria | +| Storage | S3/R2/MinIO | - | Archivos | +| Cache | Redis/BullMQ | - | Cache y colas | +| LLM | OpenRouter | MGN-020 | Inteligencia artificial | + --- ## Stack Tecnologico +> Ver detalle completo en [STACK-TECNOLOGICO.md](STACK-TECNOLOGICO.md) + ### Backend + | Tecnologia | Version | Proposito | |------------|---------|-----------| | Node.js | 20+ | Runtime | | Express.js | 4.x | Framework HTTP | | TypeScript | 5.3+ | Lenguaje | | TypeORM | 0.3.17 | ORM | -| JWT + bcryptjs | - | Autenticacion | -| Zod, class-validator | - | Validacion | -| Swagger | 3.x | Documentacion API | -| Jest | 29.x | Testing | +| BullMQ | 5.x | Colas asincronas | +| Socket.io | 4.x | WebSocket | +| Stripe SDK | Latest | Billing | ### Frontend + | Tecnologia | Version | Proposito | |------------|---------|-----------| | React | 18.x | Framework UI | | Vite | 5.x | Build tool | -| TypeScript | 5.3+ | Lenguaje | | Zustand | 4.x | State management | | Tailwind CSS | 4.x | Styling | | React Query | 5.x | Data fetching | -| React Hook Form | 7.x | Formularios | ### Database + | Tecnologia | Version | Proposito | |------------|---------|-----------| -| PostgreSQL | 15+ | Motor BD | -| RLS | - | Row-Level Security | -| uuid-ossp | - | Generacion UUIDs | -| pg_trgm | - | Busqueda fuzzy | +| PostgreSQL | 16+ | Base de datos | +| RLS | - | Multi-tenancy | +| Redis | 7.x | Cache y colas | --- ## Principios de Diseno ### 1. Multi-Tenancy First -Toda tabla tiene `tenant_id`. Todo query filtra por tenant. +Toda tabla tiene `tenant_id`. Todo query filtra por tenant via RLS. ### 2. Documentation Driven Documentar antes de desarrollar. La documentacion es el contrato. @@ -164,11 +381,20 @@ Adaptar patrones probados de Odoo al stack TypeScript. ### 5. Single Source of Truth Un lugar para cada dato. Sincronizacion automatica. +### 6. SaaS by Design +Billing, planes y self-service desde el inicio. + +### 7. AI-First Architecture +IA integrada en flujos de negocio, no como addon. + +### 8. Event-Driven Communication +Webhooks y eventos para comunicacion entre sistemas. + --- ## Entregables por Fase -### Fase 1: Foundation (Actual) +### Fase 1: Foundation (En progreso) - [ ] MGN-001 Auth completo - [ ] MGN-002 Users completo - [ ] MGN-003 Roles completo @@ -185,26 +411,46 @@ Un lugar para cada dato. Sincronizacion automatica. ### Fase 3: Extended - [ ] MGN-006 Settings - [ ] MGN-007 Audit -- [ ] MGN-008 Notifications +- [ ] MGN-008 Notifications (multicanal) - [ ] MGN-009 Reports - [ ] MGN-014 CRM - [ ] MGN-015 Projects +### Fase 4: SaaS Platform +- [ ] MGN-016 Billing (Stripe) +- [ ] MGN-017 Plans +- [ ] MGN-018 Webhooks +- [ ] MGN-019 Feature Flags +- [ ] Portal de autocontratacion + +### Fase 5: IA Intelligence +- [ ] MGN-020 AI Integration (OpenRouter) +- [ ] MGN-021 WhatsApp Business +- [ ] MGN-022 MCP Server +- [ ] Prediccion de inventario +- [ ] Asistente de negocio inteligente + --- ## Referencias | Recurso | Path | |---------|------| +| Arquitectura SaaS | [ARQUITECTURA-SAAS.md](ARQUITECTURA-SAAS.md) | +| Arquitectura IA | [ARQUITECTURA-IA.md](ARQUITECTURA-IA.md) | +| Integraciones | [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md) | +| Stack Tecnologico | [STACK-TECNOLOGICO.md](STACK-TECNOLOGICO.md) | | Directivas | `orchestration/directivas/` | -| Patrones Odoo | `orchestration/directivas/DIRECTIVA-PATRONES-ODOO.md` | | Templates | `orchestration/templates/` | -| Catálogo central | `shared/catalog/` *(patrones reutilizables)* | +| Template SaaS | `projects/template-saas/` | +| MiChangarrito | `projects/michangarrito/` | --- ## Metricas de Exito +### Metricas Core + | Metrica | Objetivo | |---------|----------| | Cobertura de tests | >80% | @@ -212,6 +458,24 @@ Un lugar para cada dato. Sincronizacion automatica. | Reutilizacion en verticales | >60% | | Tiempo de setup nueva vertical | <1 semana | +### Metricas SaaS + +| Metrica | Objetivo | +|---------|----------| +| Tiempo de onboarding self-service | <5 minutos | +| Conversion trial a paid | >10% | +| Churn mensual | <5% | +| Uptime SLA | >99.5% | + +### Metricas IA + +| Metrica | Objetivo | +|---------|----------| +| Precision de respuestas IA | >85% | +| Latencia promedio LLM | <2 segundos | +| Adopcion de chat IA | >50% usuarios activos | +| Precision prediccion inventario | >70% | + --- -*Ultima actualizacion: Diciembre 2025* +*Ultima actualizacion: Enero 2026* diff --git a/docs/00-vision-general/_MAP.md b/docs/00-vision-general/_MAP.md new file mode 100644 index 0000000..08cb3c7 --- /dev/null +++ b/docs/00-vision-general/_MAP.md @@ -0,0 +1,92 @@ +--- +id: MAP-VISION-GENERAL +title: Indice - Vision General +type: Index +status: Published +version: 1.0.0 +created_date: 2026-01-10 +updated_date: 2026-01-10 +--- + +# Vision General - Indice de Navegacion + +> Documentacion de vision, arquitectura y alcances del proyecto ERP Core + +## Contenido Principal + +| Archivo | Descripcion | Estado | Prioridad | +|---------|-------------|--------|-----------| +| [VISION-ERP-CORE.md](VISION-ERP-CORE.md) | Vision general, alcances core, SaaS e IA | Activo | P0 | +| [ARQUITECTURA-SAAS.md](ARQUITECTURA-SAAS.md) | Arquitectura de plataforma SaaS | Activo | P1 | +| [ARQUITECTURA-IA.md](ARQUITECTURA-IA.md) | Arquitectura de inteligencia artificial | Activo | P1 | +| [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md) | Catalogo de integraciones externas | Activo | P1 | +| [STACK-TECNOLOGICO.md](STACK-TECNOLOGICO.md) | Stack tecnologico detallado | Activo | P1 | + +## Estructura del Directorio + +``` +00-vision-general/ +├── _MAP.md <- Este archivo +├── VISION-ERP-CORE.md <- Documento principal +├── ARQUITECTURA-SAAS.md <- Detalles SaaS +├── ARQUITECTURA-IA.md <- Detalles IA +├── INTEGRACIONES-EXTERNAS.md <- Integraciones +└── STACK-TECNOLOGICO.md <- Stack +``` + +## Resumen de Contenido + +### VISION-ERP-CORE.md +Documento principal que define: +- Proposito y alcance del proyecto +- Modulos core (MGN-001 a MGN-022) +- Arquitectura general +- Principios de diseno +- Roadmap y entregables + +### ARQUITECTURA-SAAS.md +Detalles de la plataforma SaaS: +- Multi-tenancy con RLS +- Billing y suscripciones (Stripe) +- Planes y feature gating +- Webhooks outbound + +### ARQUITECTURA-IA.md +Detalles de inteligencia artificial: +- Gateway LLM (OpenRouter) +- MCP Server (herramientas de negocio) +- WhatsApp Business con IA +- Prediccion de inventario + +### INTEGRACIONES-EXTERNAS.md +Catalogo de integraciones: +- Stripe (billing) +- SendGrid/SES (email) +- OpenRouter (LLM) +- Meta WhatsApp Business +- S3/R2 (storage) + +### STACK-TECNOLOGICO.md +Stack completo: +- Backend (Node.js, Express, TypeScript) +- Frontend (React, Vite, Tailwind) +- Database (PostgreSQL, Redis) +- Servicios externos + +## Navegacion + +| Direccion | Destino | +|-----------|---------| +| Padre | [docs/](../_MAP.md) | +| Siguiente | [01-arquitectura/](../01-arquitectura/_MAP.md) | +| Relacionado | [02-definicion-modulos/](../02-definicion-modulos/_MAP.md) | + +## Historial de Cambios + +| Fecha | Version | Cambio | +|-------|---------|--------| +| 2026-01-10 | 1.0.0 | Creacion inicial con estructura SaaS/IA | + +--- + +*Actualizado: 2026-01-10* diff --git a/docs/01-fase-foundation/MGN-001-auth/especificaciones/ET-auth-backend.md b/docs/01-fase-foundation/MGN-001-auth/especificaciones/ET-auth-backend.md index f76e410..537b4ba 100644 --- a/docs/01-fase-foundation/MGN-001-auth/especificaciones/ET-auth-backend.md +++ b/docs/01-fase-foundation/MGN-001-auth/especificaciones/ET-auth-backend.md @@ -6,11 +6,11 @@ |-------|-------| | **Modulo** | MGN-001 | | **Nombre** | Auth - Autenticacion | -| **Version** | 1.0 | +| **Version** | 2.0 | | **Framework** | NestJS | -| **Estado** | En Diseño | +| **Estado** | Implementado | | **Autor** | System | -| **Fecha** | 2025-12-05 | +| **Fecha** | 2026-01-10 | --- @@ -19,13 +19,20 @@ ``` src/modules/auth/ ├── auth.module.ts +├── mfa.service.ts # MFA/2FA (45 tests) +├── apiKeys.service.ts # API Keys ├── controllers/ │ └── auth.controller.ts ├── services/ │ ├── auth.service.ts │ ├── token.service.ts -│ ├── password.service.ts -│ └── blacklist.service.ts +│ ├── trusted-devices.service.ts # Dispositivos confiables (41 tests) +│ ├── email-verification.service.ts # Verificacion email (32 tests) +│ └── permission-cache.service.ts # Cache permisos (37 tests) +├── providers/ +│ ├── oauth.service.ts # OAuth2 Google/Microsoft (32 tests) +│ ├── google.provider.ts +│ └── microsoft.provider.ts ├── guards/ │ ├── jwt-auth.guard.ts │ └── throttler.guard.ts @@ -40,7 +47,9 @@ src/modules/auth/ │ ├── refresh-token.dto.ts │ ├── token-response.dto.ts │ ├── request-password-reset.dto.ts -│ └── reset-password.dto.ts +│ ├── reset-password.dto.ts +│ ├── mfa.dto.ts +│ └── email-verification.dto.ts ├── interfaces/ │ ├── jwt-payload.interface.ts │ └── token-pair.interface.ts @@ -50,7 +59,16 @@ src/modules/auth/ │ ├── session-history.entity.ts │ ├── login-attempt.entity.ts │ ├── password-reset-token.entity.ts -│ └── password-history.entity.ts +│ ├── password-history.entity.ts +│ ├── user-mfa.entity.ts # MFA config por usuario +│ ├── mfa-audit-log.entity.ts # Auditoria MFA +│ ├── trusted-device.entity.ts # Dispositivos confiables +│ ├── email-verification-token.entity.ts # Tokens verificacion email +│ ├── verification-code.entity.ts # Codigos de verificacion +│ ├── api-key.entity.ts # API Keys +│ ├── oauth-provider.entity.ts # Configuracion OAuth +│ ├── oauth-state.entity.ts # Estados OAuth CSRF +│ └── oauth-user-link.entity.ts # Vinculos OAuth-Usuario └── constants/ └── auth.constants.ts ``` @@ -238,6 +256,335 @@ export class PasswordResetToken { } ``` +### UserMfa + +```typescript +// entities/user-mfa.entity.ts +export enum MfaMethod { + NONE = 'none', + TOTP = 'totp', + SMS = 'sms', + EMAIL = 'email', +} + +export enum MfaStatus { + DISABLED = 'disabled', + PENDING_SETUP = 'pending_setup', + ENABLED = 'enabled', +} + +@Entity({ schema: 'auth', name: 'user_mfa' }) +@Index('idx_user_mfa_user', ['userId'], { unique: true }) +@Index('idx_user_mfa_status', ['status'], { where: "status = 'enabled'" }) +export class UserMfa { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id', unique: true }) + userId: string; + + @Column({ type: 'enum', enum: MfaMethod, default: MfaMethod.NONE }) + method: MfaMethod; + + @Column({ type: 'enum', enum: MfaStatus, default: MfaStatus.DISABLED }) + status: MfaStatus; + + @Column({ type: 'varchar', length: 256, nullable: true, name: 'totp_secret' }) + totpSecret: string | null; + + @Column({ type: 'jsonb', default: [], name: 'backup_codes_hashes' }) + backupCodesHashes: string[]; + + @Column({ type: 'integer', default: 0, name: 'backup_codes_used' }) + backupCodesUsed: number; + + @Column({ type: 'integer', default: 0, name: 'backup_codes_total' }) + backupCodesTotal: number; + + @Column({ type: 'timestamptz', nullable: true, name: 'backup_codes_regenerated_at' }) + backupCodesRegeneratedAt: Date | null; + + @Column({ type: 'timestamptz', nullable: true, name: 'enabled_at' }) + enabledAt: Date | null; + + @Column({ type: 'timestamptz', nullable: true, name: 'last_verified_at' }) + lastVerifiedAt: Date | null; + + @Column({ type: 'integer', default: 0, name: 'failed_attempts' }) + failedAttempts: number; + + @Column({ type: 'timestamptz', nullable: true, name: 'locked_until' }) + lockedUntil: Date | null; + + @OneToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true }) + updatedAt: Date | null; +} +``` + +### TrustedDevice + +```typescript +// entities/trusted-device.entity.ts +export enum TrustLevel { + STANDARD = 'standard', // 30 dias + HIGH = 'high', // 90 dias + TEMPORARY = 'temporary', // 24 horas +} + +@Entity({ schema: 'auth', name: 'trusted_devices' }) +@Index('idx_trusted_devices_user', ['userId'], { where: 'is_active' }) +@Index('idx_trusted_devices_fingerprint', ['deviceFingerprint']) +export class TrustedDevice { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'varchar', length: 128, nullable: false, name: 'device_fingerprint' }) + deviceFingerprint: string; + + @Column({ type: 'varchar', length: 128, nullable: true, name: 'device_name' }) + deviceName: string | null; + + @Column({ type: 'varchar', length: 32, nullable: true, name: 'device_type' }) + deviceType: string | null; + + @Column({ type: 'text', nullable: true, name: 'user_agent' }) + userAgent: string | null; + + @Column({ type: 'varchar', length: 64, nullable: true, name: 'browser_name' }) + browserName: string | null; + + @Column({ type: 'varchar', length: 64, nullable: true, name: 'os_name' }) + osName: string | null; + + @Column({ type: 'inet', nullable: false, name: 'registered_ip' }) + registeredIp: string; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @Column({ type: 'enum', enum: TrustLevel, default: TrustLevel.STANDARD, name: 'trust_level' }) + trustLevel: TrustLevel; + + @Column({ type: 'timestamptz', nullable: true, name: 'trust_expires_at' }) + trustExpiresAt: Date | null; + + @Column({ type: 'timestamptz', nullable: false, name: 'last_used_at' }) + lastUsedAt: Date; + + @Column({ type: 'inet', nullable: true, name: 'last_used_ip' }) + lastUsedIp: string | null; + + @Column({ type: 'integer', default: 1, nullable: false, name: 'use_count' }) + useCount: number; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ type: 'timestamptz', nullable: true, name: 'revoked_at' }) + revokedAt: Date | null; + + @Column({ type: 'varchar', length: 128, nullable: true, name: 'revoked_reason' }) + revokedReason: string | null; +} +``` + +### EmailVerificationToken + +```typescript +// entities/email-verification-token.entity.ts +@Entity({ schema: 'auth', name: 'email_verification_tokens' }) +@Index('idx_email_verification_tokens_user_id', ['userId']) +@Index('idx_email_verification_tokens_token', ['token']) +@Index('idx_email_verification_tokens_expires_at', ['expiresAt']) +export class EmailVerificationToken { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'varchar', length: 500, unique: true, nullable: false }) + token: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + email: string; + + @Column({ type: 'timestamp', nullable: false, name: 'expires_at' }) + expiresAt: Date; + + @Column({ type: 'timestamp', nullable: true, name: 'used_at' }) + usedAt: Date | null; + + @Column({ type: 'inet', nullable: true, name: 'ip_address' }) + ipAddress: string | null; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; + + // Metodos de utilidad + isExpired(): boolean { return new Date() > this.expiresAt; } + isUsed(): boolean { return this.usedAt !== null; } + isValid(): boolean { return !this.isExpired() && !this.isUsed(); } +} +``` + +### ApiKey + +```typescript +// entities/api-key.entity.ts +@Entity({ schema: 'auth', name: 'api_keys' }) +@Index('idx_api_keys_lookup', ['keyIndex', 'isActive'], { where: 'is_active = TRUE' }) +@Index('idx_api_keys_expiration', ['expirationDate'], { where: 'expiration_date IS NOT NULL' }) +@Index('idx_api_keys_user', ['userId']) +@Index('idx_api_keys_tenant', ['tenantId']) +export class ApiKey { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: false, name: 'user_id' }) + userId: string; + + @Column({ type: 'uuid', nullable: false, name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'varchar', length: 16, nullable: false, name: 'key_index' }) + keyIndex: string; + + @Column({ type: 'varchar', length: 255, nullable: false, name: 'key_hash' }) + keyHash: string; + + @Column({ type: 'varchar', length: 100, nullable: true }) + scope: string | null; + + @Column({ type: 'inet', array: true, nullable: true, name: 'allowed_ips' }) + allowedIps: string[] | null; + + @Column({ type: 'timestamptz', nullable: true, name: 'expiration_date' }) + expirationDate: Date | null; + + @Column({ type: 'timestamptz', nullable: true, name: 'last_used_at' }) + lastUsedAt: Date | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ type: 'timestamptz', nullable: true, name: 'revoked_at' }) + revokedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'revoked_by' }) + revokedBy: string | null; +} +``` + +### OAuthProvider + +```typescript +// entities/oauth-provider.entity.ts +@Entity({ schema: 'auth', name: 'oauth_providers' }) +@Index('idx_oauth_providers_enabled', ['isEnabled']) +@Index('idx_oauth_providers_tenant', ['tenantId']) +@Index('idx_oauth_providers_code', ['code']) +export class OAuthProvider { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', nullable: true, name: 'tenant_id' }) + tenantId: string | null; + + @Column({ type: 'varchar', length: 50, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + // Configuracion OAuth2 + @Column({ type: 'varchar', length: 255, nullable: false, name: 'client_id' }) + clientId: string; + + @Column({ type: 'varchar', length: 500, nullable: true, name: 'client_secret' }) + clientSecret: string | null; + + // Endpoints OAuth2 + @Column({ type: 'varchar', length: 500, nullable: false, name: 'authorization_endpoint' }) + authorizationEndpoint: string; + + @Column({ type: 'varchar', length: 500, nullable: false, name: 'token_endpoint' }) + tokenEndpoint: string; + + @Column({ type: 'varchar', length: 500, nullable: false, name: 'userinfo_endpoint' }) + userinfoEndpoint: string; + + @Column({ type: 'varchar', length: 500, default: 'openid profile email' }) + scope: string; + + // PKCE + @Column({ type: 'boolean', default: true, name: 'pkce_enabled' }) + pkceEnabled: boolean; + + @Column({ type: 'varchar', length: 10, default: 'S256', name: 'code_challenge_method' }) + codeChallengeMethod: string | null; + + // Mapeo de claims + @Column({ type: 'jsonb', name: 'claim_mapping', default: { sub: 'oauth_uid', email: 'email', name: 'name', picture: 'avatar_url' } }) + claimMapping: Record; + + // UI + @Column({ type: 'varchar', length: 100, nullable: true, name: 'button_text' }) + buttonText: string | null; + + @Column({ type: 'boolean', default: false, name: 'is_enabled' }) + isEnabled: boolean; + + @Column({ type: 'text', array: true, nullable: true, name: 'allowed_domains' }) + allowedDomains: string[] | null; + + @Column({ type: 'boolean', default: false, name: 'auto_create_users' }) + autoCreateUsers: boolean; + + @ManyToOne(() => Tenant, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'tenant_id' }) + tenant: Tenant | null; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} +``` + --- ## Interfaces @@ -893,7 +1240,7 @@ import { v4 as uuidv4 } from 'uuid'; import { RefreshToken } from '../entities/refresh-token.entity'; import { JwtPayload, JwtRefreshPayload } from '../interfaces/jwt-payload.interface'; import { TokenPair } from '../interfaces/token-pair.interface'; -import { BlacklistService } from './blacklist.service'; +import { PermissionCacheService } from './permission-cache.service'; @Injectable() export class TokenService { @@ -905,7 +1252,7 @@ export class TokenService { private readonly configService: ConfigService, @InjectRepository(RefreshToken) private readonly refreshTokenRepository: Repository, - private readonly blacklistService: BlacklistService, + private readonly permissionCacheService: PermissionCacheService, ) {} async generateTokenPair(user: any, metadata: any): Promise { @@ -1053,13 +1400,12 @@ export class TokenService { ); } - async blacklistAccessToken(jti: string, expiresIn?: number): Promise { - const ttl = expiresIn || 900; // Default 15 min - await this.blacklistService.blacklist(jti, ttl); - } - - async isAccessTokenBlacklisted(jti: string): Promise { - return this.blacklistService.isBlacklisted(jti); + /** + * Invalida el cache de permisos para un usuario + * Se llama cuando se revocan tokens o cambian permisos + */ + async invalidatePermissionCache(userId: string): Promise { + await this.permissionCacheService.invalidateAllForUser(userId); } private async generateTokenPairWithFamily( @@ -1080,231 +1426,493 @@ export class TokenService { } ``` -### PasswordService +### MfaService ```typescript -// services/password.service.ts -import { Injectable, BadRequestException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, IsNull, MoreThan } from 'typeorm'; -import * as crypto from 'crypto'; -import * as bcrypt from 'bcrypt'; -import { User } from '../../users/entities/user.entity'; -import { PasswordResetToken } from '../entities/password-reset-token.entity'; -import { PasswordHistory } from '../entities/password-history.entity'; -import { EmailService } from '../../notifications/services/email.service'; -import { TokenService } from './token.service'; +// mfa.service.ts +/** + * Servicio de Autenticacion Multi-Factor (MFA/2FA) + * + * Funcionalidades: + * - Configuracion TOTP con QR code + * - Verificacion de codigos TOTP + * - Gestion de codigos de respaldo (backup codes) + * - Auditoria de eventos MFA + * - Bloqueo por intentos fallidos + * + * Tests: 45 tests en mfa.service.spec.ts + */ + +// Configuracion +const BACKUP_CODES_COUNT = 10; +const MAX_FAILED_ATTEMPTS = 5; +const LOCKOUT_DURATION_MINUTES = 15; +const TOTP_WINDOW = 1; // Tolerancia de 1 paso para drift de tiempo + +export interface MfaRequestContext { + ipAddress: string; + userAgent: string; + deviceFingerprint?: string; +} @Injectable() -export class PasswordService { - private readonly TOKEN_EXPIRY_HOURS = 1; - private readonly MAX_ATTEMPTS = 3; - private readonly PASSWORD_HISTORY_LIMIT = 5; - +export class MfaService { constructor( - @InjectRepository(User) - private readonly userRepository: Repository, - @InjectRepository(PasswordResetToken) - private readonly resetTokenRepository: Repository, - @InjectRepository(PasswordHistory) - private readonly passwordHistoryRepository: Repository, - private readonly emailService: EmailService, - private readonly tokenService: TokenService, + @InjectRepository(User) private readonly userRepository: Repository, + @InjectRepository(UserMfa) private readonly userMfaRepository: Repository, + @InjectRepository(MfaAuditLog) private readonly mfaAuditLogRepository: Repository, ) {} - async requestPasswordReset(email: string): Promise { - const user = await this.userRepository.findOne({ - where: { email: email.toLowerCase() }, - }); + /** + * Inicia configuracion MFA para un usuario + * Genera secret TOTP, QR code y codigos de respaldo + */ + async initiateMfaSetup(userId: string, context: MfaRequestContext): Promise { + // Genera secret TOTP + const secret = authenticator.generateSecret(); + const otpauthUri = authenticator.keyuri(user.email, 'ERP Generic', secret); + const qrCodeDataUrl = await QRCode.toDataURL(otpauthUri); - // No revelar si el email existe - if (!user) { - return; - } + // Genera codigos de respaldo (10 codigos formato XXXX-XXXX) + const { codes, hashes } = await this.generateBackupCodes(); - // Invalidar tokens anteriores - await this.resetTokenRepository.update( - { userId: user.id, usedAt: IsNull(), invalidatedAt: IsNull() }, - { invalidatedAt: new Date() }, - ); + // Crea/actualiza registro con estado PENDING_SETUP + // ... - // Generar nuevo token - const token = crypto.randomBytes(32).toString('hex'); - const tokenHash = await bcrypt.hash(token, 10); - const expiresAt = new Date(Date.now() + this.TOKEN_EXPIRY_HOURS * 60 * 60 * 1000); - - await this.resetTokenRepository.save({ - userId: user.id, - tenantId: user.tenantId, - tokenHash, - expiresAt, - }); - - // Enviar email - await this.emailService.sendPasswordResetEmail(user.email, token, user.firstName); + return { secret, otpauthUri, qrCodeDataUrl, backupCodes: codes }; } - async validateResetToken(token: string): Promise<{ valid: boolean; email?: string; reason?: string }> { - const resetTokens = await this.resetTokenRepository.find({ - where: { - usedAt: IsNull(), - invalidatedAt: IsNull(), - expiresAt: MoreThan(new Date()), - }, - }); + /** + * Habilita MFA despues de verificar codigo de configuracion + */ + async enableMfa(userId: string, code: string, context: MfaRequestContext): Promise; - for (const resetToken of resetTokens) { - const isMatch = await bcrypt.compare(token, resetToken.tokenHash); - if (isMatch) { - if (resetToken.attempts >= this.MAX_ATTEMPTS) { - return { valid: false, reason: 'invalid' }; - } + /** + * Verifica codigo TOTP o codigo de respaldo + * Incrementa intentos fallidos y bloquea si excede limite + */ + async verifyTotp(userId: string, code: string, context: MfaRequestContext): Promise; - const user = await this.userRepository.findOne({ - where: { id: resetToken.userId }, - }); + /** + * Deshabilita MFA (requiere password + codigo TOTP/backup) + */ + async disableMfa(userId: string, password: string, code: string, context: MfaRequestContext): Promise; - return { - valid: true, - email: this.maskEmail(user?.email || ''), - }; - } - } + /** + * Regenera codigos de respaldo (requiere verificacion TOTP) + */ + async regenerateBackupCodes(userId: string, code: string, context: MfaRequestContext): Promise; - return { valid: false, reason: 'invalid' }; - } + /** + * Obtiene estado MFA de un usuario + */ + async getMfaStatus(userId: string): Promise; - async resetPassword(token: string, newPassword: string): Promise { - // 1. Buscar token válido - const resetTokens = await this.resetTokenRepository.find({ - where: { - usedAt: IsNull(), - invalidatedAt: IsNull(), - }, - }); - - let matchedToken: PasswordResetToken | null = null; - - for (const resetToken of resetTokens) { - const isMatch = await bcrypt.compare(token, resetToken.tokenHash); - if (isMatch) { - matchedToken = resetToken; - break; - } - } - - if (!matchedToken) { - throw new BadRequestException('Token de recuperación inválido'); - } - - // 2. Verificar expiración - if (new Date() > matchedToken.expiresAt) { - throw new BadRequestException('Token de recuperación expirado'); - } - - // 3. Verificar intentos - if (matchedToken.attempts >= this.MAX_ATTEMPTS) { - matchedToken.invalidatedAt = new Date(); - await this.resetTokenRepository.save(matchedToken); - throw new BadRequestException('Token invalidado por demasiados intentos'); - } - - // 4. Obtener usuario - const user = await this.userRepository.findOne({ - where: { id: matchedToken.userId }, - }); - - if (!user) { - throw new BadRequestException('Usuario no encontrado'); - } - - // 5. Verificar que no sea password anterior - const isReused = await this.isPasswordReused(user.id, newPassword); - if (isReused) { - matchedToken.attempts += 1; - await this.resetTokenRepository.save(matchedToken); - throw new BadRequestException('No puedes usar una contraseña anterior'); - } - - // 6. Hashear nuevo password - const passwordHash = await bcrypt.hash(newPassword, 12); - - // 7. Guardar en historial - await this.passwordHistoryRepository.save({ - userId: user.id, - tenantId: user.tenantId, - passwordHash, - }); - - // 8. Actualizar usuario - user.passwordHash = passwordHash; - await this.userRepository.save(user); - - // 9. Marcar token como usado - matchedToken.usedAt = new Date(); - await this.resetTokenRepository.save(matchedToken); - - // 10. Revocar todas las sesiones - await this.tokenService.revokeAllUserTokens(user.id, 'password_change'); - - // 11. Enviar email de confirmación - await this.emailService.sendPasswordChangedEmail(user.email, user.firstName); - } - - private async isPasswordReused(userId: string, newPassword: string): Promise { - const history = await this.passwordHistoryRepository.find({ - where: { userId }, - order: { createdAt: 'DESC' }, - take: this.PASSWORD_HISTORY_LIMIT, - }); - - for (const record of history) { - const isMatch = await bcrypt.compare(newPassword, record.passwordHash); - if (isMatch) return true; - } - - return false; - } - - private maskEmail(email: string): string { - const [local, domain] = email.split('@'); - const maskedLocal = local.charAt(0) + '***'; - return `${maskedLocal}@${domain}`; - } + /** + * Verifica si el usuario tiene MFA habilitado + */ + async isMfaEnabled(userId: string): Promise; } ``` -### BlacklistService +### TrustedDevicesService ```typescript -// services/blacklist.service.ts -import { Injectable } from '@nestjs/common'; -import { InjectRedis } from '@nestjs-modules/ioredis'; -import Redis from 'ioredis'; +// services/trusted-devices.service.ts +/** + * Servicio de Gestion de Dispositivos Confiables + * + * Permite bypass de MFA para dispositivos previamente autenticados. + * Niveles de confianza: STANDARD (30d), HIGH (90d), TEMPORARY (24h) + * + * Tests: 41 tests en trusted-devices.service.spec.ts + */ + +const DEFAULT_TRUST_DURATION_DAYS = 30; +const HIGH_TRUST_DURATION_DAYS = 90; +const TEMPORARY_TRUST_DURATION_HOURS = 24; +const MAX_TRUSTED_DEVICES_PER_USER = 10; + +export interface DeviceInfo { + userAgent: string; + deviceName?: string; + deviceType?: string; + browserName?: string; + osName?: string; +} @Injectable() -export class BlacklistService { - private readonly PREFIX = 'token:blacklist:'; - +export class TrustedDevicesService { constructor( - @InjectRedis() private readonly redis: Redis, + @InjectRepository(TrustedDevice) private readonly trustedDeviceRepository: Repository, + @InjectRepository(User) private readonly userRepository: Repository, ) {} - async blacklist(jti: string, ttlSeconds: number): Promise { - const key = `${this.PREFIX}${jti}`; - await this.redis.set(key, '1', 'EX', ttlSeconds); + /** + * Genera fingerprint del dispositivo (hash SHA256 de userAgent + IP) + */ + generateFingerprint(deviceInfo: DeviceInfo, ipAddress: string): string; + + /** + * Agrega dispositivo confiable para un usuario + * Si excede limite (10), elimina el mas antiguo + */ + async addDevice( + userId: string, + deviceInfo: DeviceInfo, + context: RequestContext, + trustLevel: TrustLevel = TrustLevel.STANDARD + ): Promise; + + /** + * Verifica si un dispositivo es confiable + * Actualiza last_used_at y use_count + */ + async verifyDevice(userId: string, deviceInfo: DeviceInfo, ipAddress: string): Promise; + + /** + * Lista todos los dispositivos confiables de un usuario + */ + async listDevices(userId: string, currentFingerprint?: string): Promise; + + /** + * Revoca un dispositivo especifico + */ + async revokeDevice(userId: string, deviceId: string, reason?: string): Promise; + + /** + * Revoca todos los dispositivos de un usuario (seguridad) + */ + async revokeAllDevices(userId: string, reason?: string): Promise; + + /** + * Limpieza de dispositivos expirados (cron job) + */ + async cleanupExpiredDevices(): Promise; + + /** + * Actualiza nivel de confianza de un dispositivo + */ + async updateTrustLevel(userId: string, deviceId: string, trustLevel: TrustLevel): Promise; +} +``` + +### EmailVerificationService + +```typescript +// services/email-verification.service.ts +/** + * Servicio de Verificacion de Email + * + * Maneja el flujo de verificacion de email para nuevos usuarios: + * - Generacion de tokens seguros (crypto.randomBytes) + * - Envio de emails de verificacion + * - Verificacion de tokens y activacion de cuentas + * - Rate limiting para reenvios (2 min cooldown) + * + * Tests: 32 tests en email-verification.service.spec.ts + */ + +@Injectable() +export class EmailVerificationService { + private readonly TOKEN_EXPIRY_HOURS = 24; + private readonly RESEND_COOLDOWN_MINUTES = 2; + + constructor( + @InjectRepository(User) private readonly userRepository: Repository, + @InjectRepository(EmailVerificationToken) private readonly tokenRepository: Repository, + private readonly emailService: EmailService, + ) {} + + /** + * Genera token de verificacion seguro (32 bytes hex) + */ + generateVerificationToken(): string { + return crypto.randomBytes(32).toString('hex'); } - async isBlacklisted(jti: string): Promise { - const key = `${this.PREFIX}${jti}`; - const result = await this.redis.get(key); - return result !== null; + /** + * Envia email de verificacion al usuario + * Invalida tokens anteriores antes de crear uno nuevo + */ + async sendVerificationEmail(userId: string, email: string, ipAddress?: string): Promise; + + /** + * Verifica email con token + * Marca token como usado y activa usuario si estaba PENDING_VERIFICATION + */ + async verifyEmail(token: string, ipAddress?: string): Promise; + + /** + * Reenvia email de verificacion con rate limiting + */ + async resendVerificationEmail(userId: string, ipAddress?: string): Promise; + + /** + * Obtiene estado de verificacion de email + */ + async getVerificationStatus(userId: string): Promise; + + /** + * Limpieza de tokens expirados (cron job) + */ + async cleanupExpiredTokens(): Promise; +} +``` + +### PermissionCacheService + +```typescript +// services/permission-cache.service.ts +/** + * Servicio de Cache de Permisos en Redis + * + * Proporciona lookups rapidos de permisos con fallback graceful + * cuando Redis no esta disponible. + * + * Tests: 37 tests en permission-cache.service.spec.ts + */ + +@Injectable() +export class PermissionCacheService { + readonly DEFAULT_TTL = 300; // 5 minutos + readonly KEY_PREFIX = 'perm:'; + + constructor(@InjectRedis() private readonly redis: Redis) {} + + // ===== Cache de Permisos ===== + + /** + * Obtiene permisos cacheados para un usuario + */ + async getUserPermissions(userId: string): Promise; + + /** + * Cachea permisos de un usuario + */ + async setUserPermissions(userId: string, permissions: string[], ttl?: number): Promise; + + /** + * Invalida cache de permisos de un usuario + */ + async invalidateUserPermissions(userId: string): Promise; + + // ===== Cache de Roles ===== + + /** + * Obtiene roles cacheados para un usuario + */ + async getUserRoles(userId: string): Promise; + + /** + * Cachea roles de un usuario + */ + async setUserRoles(userId: string, roles: string[], ttl?: number): Promise; + + /** + * Invalida cache de roles de un usuario + */ + async invalidateUserRoles(userId: string): Promise; + + // ===== Verificacion de Permisos ===== + + /** + * Verifica si usuario tiene un permiso especifico + */ + async hasPermission(userId: string, permission: string): Promise; + + /** + * Verifica si usuario tiene alguno de los permisos + */ + async hasAnyPermission(userId: string, permissions: string[]): Promise; + + /** + * Verifica si usuario tiene todos los permisos + */ + async hasAllPermissions(userId: string, permissions: string[]): Promise; + + // ===== Invalidacion Masiva ===== + + /** + * Invalida todo el cache (permisos y roles) de un usuario + */ + async invalidateAllForUser(userId: string): Promise; + + /** + * Invalida todo el cache de un tenant (usando SCAN) + */ + async invalidateAllForTenant(tenantId: string): Promise; +} +``` + +### ApiKeysService + +```typescript +// apiKeys.service.ts +/** + * Servicio de Gestion de API Keys + * + * Permite autenticacion programatica mediante API keys: + * - Generacion de keys seguras con PBKDF2 + * - Validacion con whitelist de IPs + * - Expiracion configurable + * - Scopes para control de acceso + */ + +const API_KEY_PREFIX = 'mgn_'; +const KEY_LENGTH = 32; // 256 bits +const HASH_ITERATIONS = 100000; +const HASH_DIGEST = 'sha512'; + +export interface CreateApiKeyDto { + user_id: string; + tenant_id: string; + name: string; + scope?: string; + allowed_ips?: string[]; + expiration_days?: number; +} + +@Injectable() +export class ApiKeysService { + /** + * Crea una nueva API key + * La key plain solo se retorna una vez, no puede recuperarse + */ + async create(dto: CreateApiKeyDto): Promise; + + /** + * Lista todas las API keys de un usuario/tenant + */ + async findAll(filters: ApiKeyFilters): Promise; + + /** + * Busca API key por ID + */ + async findById(id: string, tenantId: string): Promise; + + /** + * Actualiza una API key + */ + async update(id: string, tenantId: string, dto: UpdateApiKeyDto): Promise; + + /** + * Revoca (soft delete) una API key + */ + async revoke(id: string, tenantId: string): Promise; + + /** + * Elimina permanentemente una API key + */ + async delete(id: string, tenantId: string): Promise; + + /** + * Valida una API key y retorna info del usuario + * Usado por el middleware de autenticacion + */ + async validate(plainKey: string, clientIp?: string): Promise; + + /** + * Regenera una API key (invalida la anterior) + */ + async regenerate(id: string, tenantId: string): Promise; +} +``` + +### OAuthService + +```typescript +// providers/oauth.service.ts +/** + * Servicio de Autenticacion OAuth2 + * + * Soporta Google y Microsoft con: + * - PKCE para seguridad adicional + * - State management para proteccion CSRF + * - Creacion/vinculacion automatica de usuarios + * - Configuracion por tenant + * + * Tests: 32 tests en oauth.service.spec.ts + */ + +const STATE_EXPIRY_MINUTES = 10; + +export type OAuthProviderType = 'google' | 'microsoft'; + +export interface OAuthLoginResult { + isNewUser: boolean; + userId: string; + tenantId: string; + accessToken: string; + refreshToken: string; + sessionId: string; + expiresAt: Date; +} + +@Injectable() +export class OAuthService { + private providers: Map = new Map(); + + constructor( + @InjectRepository(User) private readonly userRepository: Repository, + @InjectRepository(OAuthProvider) private readonly providerRepository: Repository, + @InjectRepository(OAuthUserLink) private readonly userLinkRepository: Repository, + @InjectRepository(OAuthState) private readonly stateRepository: Repository, + @InjectRepository(Tenant) private readonly tenantRepository: Repository, + private readonly tokenService: TokenService, + ) {} + + // ===== PKCE Helpers ===== + + private generateCodeVerifier(): string { + return crypto.randomBytes(32).toString('base64url'); } - async removeFromBlacklist(jti: string): Promise { - const key = `${this.PREFIX}${jti}`; - await this.redis.del(key); + private generateCodeChallenge(verifier: string): string { + return crypto.createHash('sha256').update(verifier).digest('base64url'); } + + // ===== OAuth Flow ===== + + /** + * Inicia flujo OAuth - genera URL de autorizacion + * Guarda state con PKCE code_verifier en BD + */ + async initiateOAuth( + providerType: OAuthProviderType, + returnUrl: string | undefined, + metadata: RequestMetadata, + linkUserId?: string + ): Promise<{ authUrl: string; state: string }>; + + /** + * Maneja callback OAuth + * Valida state, intercambia code por tokens, crea/vincula usuario + */ + async handleCallback( + providerType: OAuthProviderType, + code: string, + state: string, + metadata: RequestMetadata + ): Promise; + + // ===== Gestion de Cuentas ===== + + /** + * Obtiene links OAuth de un usuario + */ + async getUserOAuthLinks(userId: string): Promise; + + /** + * Desvincula proveedor OAuth de un usuario + * No permite si es el unico metodo de autenticacion + */ + async unlinkProvider(userId: string, providerId: string): Promise; + + /** + * Limpieza de estados OAuth expirados (cron job) + */ + async cleanupExpiredStates(): Promise; } ``` @@ -1323,7 +1931,11 @@ import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagg import { Throttle } from '@nestjs/throttler'; import { AuthService } from '../services/auth.service'; import { TokenService } from '../services/token.service'; -import { PasswordService } from '../services/password.service'; +import { MfaService } from '../mfa.service'; +import { TrustedDevicesService } from '../services/trusted-devices.service'; +import { EmailVerificationService } from '../services/email-verification.service'; +import { ApiKeysService } from '../apiKeys.service'; +import { OAuthService } from '../providers/oauth.service'; import { LoginDto } from '../dto/login.dto'; import { LoginResponseDto } from '../dto/login-response.dto'; import { RefreshTokenDto } from '../dto/refresh-token.dto'; @@ -1340,7 +1952,11 @@ export class AuthController { constructor( private readonly authService: AuthService, private readonly tokenService: TokenService, - private readonly passwordService: PasswordService, + private readonly mfaService: MfaService, + private readonly trustedDevicesService: TrustedDevicesService, + private readonly emailVerificationService: EmailVerificationService, + private readonly apiKeysService: ApiKeysService, + private readonly oauthService: OAuthService, ) {} @Post('login') @@ -1516,13 +2132,13 @@ import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/com import { AuthGuard } from '@nestjs/passport'; import { Reflector } from '@nestjs/core'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; -import { BlacklistService } from '../services/blacklist.service'; +import { TokenService } from '../services/token.service'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { constructor( private reflector: Reflector, - private blacklistService: BlacklistService, + private tokenService: TokenService, ) { super(); } @@ -1544,23 +2160,19 @@ export class JwtAuthGuard extends AuthGuard('jwt') { return false; } - // Check if token is blacklisted + // Verificar que el token no haya sido revocado const request = context.switchToHttp().getRequest(); const user = request.user; - if (user?.jti) { - const isBlacklisted = await this.blacklistService.isBlacklisted(user.jti); - if (isBlacklisted) { - throw new UnauthorizedException('Token revocado'); - } - } + // La validacion de tokens revocados se hace en el TokenService + // mediante consulta a la BD de refresh_tokens return true; } handleRequest(err: any, user: any, info: any) { if (err || !user) { - throw err || new UnauthorizedException('Token inválido o expirado'); + throw err || new UnauthorizedException('Token invalido o expirado'); } return user; } @@ -1632,16 +2244,29 @@ import { ThrottlerModule } from '@nestjs/throttler'; import { AuthController } from './controllers/auth.controller'; import { AuthService } from './services/auth.service'; import { TokenService } from './services/token.service'; -import { PasswordService } from './services/password.service'; -import { BlacklistService } from './services/blacklist.service'; +import { MfaService } from './mfa.service'; +import { TrustedDevicesService } from './services/trusted-devices.service'; +import { EmailVerificationService } from './services/email-verification.service'; +import { PermissionCacheService } from './services/permission-cache.service'; +import { ApiKeysService } from './apiKeys.service'; +import { OAuthService } from './providers/oauth.service'; import { JwtStrategy } from './strategies/jwt.strategy'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; +// Entidades import { RefreshToken } from './entities/refresh-token.entity'; import { RevokedToken } from './entities/revoked-token.entity'; import { SessionHistory } from './entities/session-history.entity'; import { LoginAttempt } from './entities/login-attempt.entity'; import { PasswordResetToken } from './entities/password-reset-token.entity'; import { PasswordHistory } from './entities/password-history.entity'; +import { UserMfa } from './entities/user-mfa.entity'; +import { MfaAuditLog } from './entities/mfa-audit-log.entity'; +import { TrustedDevice } from './entities/trusted-device.entity'; +import { EmailVerificationToken } from './entities/email-verification-token.entity'; +import { ApiKey } from './entities/api-key.entity'; +import { OAuthProvider } from './entities/oauth-provider.entity'; +import { OAuthState } from './entities/oauth-state.entity'; +import { OAuthUserLink } from './entities/oauth-user-link.entity'; @Module({ imports: [ @@ -1652,6 +2277,14 @@ import { PasswordHistory } from './entities/password-history.entity'; LoginAttempt, PasswordResetToken, PasswordHistory, + UserMfa, + MfaAuditLog, + TrustedDevice, + EmailVerificationToken, + ApiKey, + OAuthProvider, + OAuthState, + OAuthUserLink, ]), PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.register({}), @@ -1664,14 +2297,24 @@ import { PasswordHistory } from './entities/password-history.entity'; providers: [ AuthService, TokenService, - PasswordService, - BlacklistService, + MfaService, + TrustedDevicesService, + EmailVerificationService, + PermissionCacheService, + ApiKeysService, + OAuthService, JwtStrategy, JwtAuthGuard, ], exports: [ AuthService, TokenService, + MfaService, + TrustedDevicesService, + EmailVerificationService, + PermissionCacheService, + ApiKeysService, + OAuthService, JwtAuthGuard, ], }) @@ -1736,7 +2379,8 @@ export class AuthModule {} | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creación inicial | +| 1.0 | 2025-12-05 | System | Creacion inicial | +| 2.0 | 2026-01-10 | System | Documentacion de servicios implementados: MfaService (45 tests), TrustedDevicesService (41 tests), EmailVerificationService (32 tests), PermissionCacheService (37 tests), ApiKeysService, OAuthService (32 tests). Eliminacion de referencias a servicios inexistentes (password.service.ts, blacklist.service.ts). Adicion de nuevas entidades: UserMfa, TrustedDevice, EmailVerificationToken, ApiKey, OAuthProvider. | --- diff --git a/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-001.md b/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-001.md deleted file mode 100644 index 24f47fe..0000000 --- a/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-001.md +++ /dev/null @@ -1,296 +0,0 @@ -# US-MGN001-001: Login con Email y Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN001-001 | -| **Modulo** | MGN-001 Auth | -| **Sprint** | Sprint 1 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema ERP -**Quiero** poder iniciar sesion con mi email y contraseña -**Para** acceder a las funcionalidades del sistema de forma segura - ---- - -## Descripcion - -El usuario necesita un mecanismo seguro para autenticarse en el sistema utilizando sus credenciales (email y contraseña). Al autenticarse exitosamente, recibira tokens JWT que le permitiran acceder a los recursos protegidos. - -### Contexto - -- Primera interaccion del usuario con el sistema -- Puerta de entrada a todas las funcionalidades -- Base de la seguridad del sistema - ---- - -## Criterios de Aceptacion - -### Escenario 1: Login exitoso - -```gherkin -Given un usuario registrado con email "user@example.com" - And password "SecurePass123!" - And el usuario no esta bloqueado - And el usuario esta activo -When el usuario envia sus credenciales al endpoint /api/v1/auth/login -Then el sistema responde con status 200 - And el body contiene "accessToken" de tipo string - And el body contiene "refreshToken" de tipo string - And el body contiene "user" con id, email, firstName, lastName, roles - And se establece cookie httpOnly "refresh_token" - And se registra el login en session_history -``` - -### Escenario 2: Login con email incorrecto - -```gherkin -Given un email "noexiste@example.com" que no existe en el sistema -When el usuario intenta hacer login con ese email -Then el sistema responde con status 401 - And el mensaje es "Credenciales invalidas" - And NO se revela que el email no existe - And se registra el intento fallido en login_attempts -``` - -### Escenario 3: Login con password incorrecto - -```gherkin -Given un usuario registrado con email "user@example.com" - And el password correcto es "SecurePass123!" -When el usuario intenta hacer login con password "wrongpassword" -Then el sistema responde con status 401 - And el mensaje es "Credenciales invalidas" - And se incrementa failed_login_attempts del usuario - And se registra el intento fallido en login_attempts -``` - -### Escenario 4: Cuenta bloqueada por intentos fallidos - -```gherkin -Given un usuario con email "user@example.com" - And el usuario tiene 5 intentos fallidos de login - And el campo locked_until es una fecha futura -When el usuario intenta hacer login con credenciales correctas -Then el sistema responde con status 423 - And el mensaje indica "Cuenta bloqueada" - And se indica el tiempo restante de bloqueo -``` - -### Escenario 5: Cuenta inactiva - -```gherkin -Given un usuario con email "inactive@example.com" - And el campo is_active del usuario es false -When el usuario intenta hacer login -Then el sistema responde con status 403 - And el mensaje es "Cuenta inactiva" -``` - -### Escenario 6: Validacion de campos - -```gherkin -Given un request al endpoint de login -When el email no es un email valido -Then el sistema responde con status 400 - And el mensaje indica "Email invalido" - -When el password tiene menos de 8 caracteres -Then el sistema responde con status 400 - And el mensaje indica "Password debe tener minimo 8 caracteres" - -When el email esta vacio -Then el sistema responde con status 400 - And el mensaje indica "Email es requerido" -``` - -### Escenario 7: Desbloqueo automatico - -```gherkin -Given un usuario con cuenta bloqueada - And el tiempo de bloqueo (30 minutos) ha pasado -When el usuario intenta hacer login con credenciales correctas -Then el sistema permite el login - And resetea failed_login_attempts a 0 - And limpia locked_until -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -| | -| ╔═══════════════════════════╗ | -| ║ INICIAR SESION ║ | -| ╚═══════════════════════════╝ | -| | -| Correo electronico | -| +---------------------------+ | -| | user@example.com | | -| +---------------------------+ | -| | -| Contraseña | -| +---------------------------+ | -| | •••••••••••• | [👁] | -| +---------------------------+ | -| | -| [ ] Recordar sesion | -| | -| [===== INICIAR SESION =====] | -| | -| ¿Olvidaste tu contraseña? | -| | -+------------------------------------------------------------------+ - -Estados de UI: -┌─────────────────────────────────────────────────────────────────┐ -│ Estado: Loading │ -│ - Boton deshabilitado │ -│ - Spinner visible en boton │ -│ - Campos deshabilitados │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ Estado: Error │ -│ - Toast rojo con mensaje de error │ -│ - Campos con borde rojo si invalidos │ -│ - Texto de ayuda debajo del campo │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ Estado: Bloqueado │ -│ - Mensaje: "Cuenta bloqueada. Intenta en XX minutos" │ -│ - Enlace a "¿Necesitas ayuda?" │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API - -```typescript -// Request -POST /api/v1/auth/login -Content-Type: application/json -X-Tenant-Id: optional-if-single-tenant - -{ - "email": "user@example.com", - "password": "SecurePass123!" -} - -// Response 200 -{ - "accessToken": "eyJhbGciOiJSUzI1NiIs...", - "refreshToken": "eyJhbGciOiJSUzI1NiIs...", - "tokenType": "Bearer", - "expiresIn": 900, - "user": { - "id": "uuid", - "email": "user@example.com", - "firstName": "John", - "lastName": "Doe", - "roles": ["admin"] - } -} - -// Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict; Max-Age=604800 -``` - -### Validaciones - -| Campo | Regla | Mensaje Error | -|-------|-------|---------------| -| email | Required, IsEmail | "Email es requerido" / "Email invalido" | -| password | Required, MinLength(8), MaxLength(128) | "Password es requerido" / "Password debe tener minimo 8 caracteres" | - -### Seguridad - -- Bcrypt con salt rounds = 12 -- Rate limiting: 10 requests/minuto por IP -- No revelar si email existe -- Tokens firmados con RS256 - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/auth/login implementado -- [ ] Validaciones de DTO funcionando -- [ ] Login exitoso retorna tokens JWT -- [ ] Login fallido registra intento -- [ ] Bloqueo despues de 5 intentos -- [ ] Desbloqueo automatico despues de 30 min -- [ ] Cookie httpOnly para refresh token -- [ ] Registro en session_history -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Documentacion Swagger actualizada -- [ ] Code review aprobado - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla users | Con campos email, password_hash, is_active | -| Tabla login_attempts | Para registro de intentos | -| Tabla session_history | Para auditoria | -| TokenService | Para generar tokens JWT | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN001-002 | Logout (necesita sesion) | -| US-MGN001-003 | Refresh token (necesita login) | -| Todas las features | Requieren autenticacion | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: AuthService.login() | 4h | -| Backend: Validaciones y errores | 2h | -| Backend: Tests unitarios | 3h | -| Frontend: LoginPage | 4h | -| Frontend: authStore | 2h | -| Frontend: Tests | 2h | -| **Total** | **17h** | - ---- - -## Referencias - -- [RF-AUTH-001](../../01-requerimientos/RF-auth/RF-AUTH-001.md) - Requerimiento funcional -- [DDL-SPEC-core_auth](../../02-modelado/database-design/DDL-SPEC-core_auth.md) - Esquema BD -- [ET-auth-backend](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Spec tecnica - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-002.md b/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-002.md deleted file mode 100644 index 59860a1..0000000 --- a/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-002.md +++ /dev/null @@ -1,261 +0,0 @@ -# US-MGN001-002: Logout y Cierre de Sesion - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN001-002 | -| **Modulo** | MGN-001 Auth | -| **Sprint** | Sprint 1 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 5 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema ERP -**Quiero** poder cerrar mi sesion de forma segura -**Para** proteger mi cuenta cuando dejo de usar el sistema o en dispositivos compartidos - ---- - -## Descripcion - -El usuario necesita poder cerrar su sesion actual, revocando los tokens de acceso para que no puedan ser reutilizados. Tambien debe poder cerrar todas sus sesiones activas en caso de sospecha de compromiso de cuenta. - -### Contexto - -- Seguridad en dispositivos compartidos -- Cumplimiento de politicas corporativas -- Proteccion contra robo de tokens - ---- - -## Criterios de Aceptacion - -### Escenario 1: Logout exitoso - -```gherkin -Given un usuario autenticado con sesion activa - And tiene un access token valido - And tiene un refresh token en cookie -When el usuario hace POST /api/v1/auth/logout -Then el sistema responde con status 200 - And el mensaje es "Sesion cerrada exitosamente" - And el refresh token es revocado en BD - And el access token es agregado a la blacklist - And la cookie refresh_token es eliminada - And se registra el logout en session_history -``` - -### Escenario 2: Logout de todas las sesiones - -```gherkin -Given un usuario autenticado - And tiene 3 sesiones activas en diferentes dispositivos -When el usuario hace POST /api/v1/auth/logout-all -Then el sistema responde con status 200 - And el mensaje es "Todas las sesiones han sido cerradas" - And el response incluye "sessionsRevoked": 3 - And TODOS los refresh tokens del usuario son revocados - And se registra el logout_all en session_history -``` - -### Escenario 3: Logout sin autenticacion - -```gherkin -Given un usuario sin token de acceso -When intenta hacer logout -Then el sistema responde con status 401 - And el mensaje es "Token requerido" -``` - -### Escenario 4: Logout con token expirado - -```gherkin -Given un usuario con access token expirado - And tiene refresh token valido en cookie -When intenta hacer logout -Then el sistema permite el logout usando solo el refresh token - And responde con status 200 -``` - -### Escenario 5: Verificacion post-logout - -```gherkin -Given un usuario que acaba de hacer logout -When intenta acceder a un endpoint protegido - With el access token anterior -Then el sistema responde con status 401 - And el mensaje es "Token revocado" -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] [Usuario ▼] | -+------------------------------------------------------------------+ - ┌─────────────────┐ - │ Mi Perfil │ - │ Configuracion │ - │ ─────────────── │ - │ Cerrar Sesion │ ← Click - │ Cerrar Todas │ - └─────────────────┘ - -┌──────────────────────────────────────────────────────────────────┐ -│ CONFIRMACION DE LOGOUT │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ ¿Estas seguro que deseas cerrar sesion? │ -│ │ -│ [ Cancelar ] [ Cerrar Sesion ] │ -│ │ -└──────────────────────────────────────────────────────────────────┘ - -┌──────────────────────────────────────────────────────────────────┐ -│ CERRAR TODAS LAS SESIONES │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ ⚠️ Esta accion cerrara tu sesion en todos los dispositivos. │ -│ │ -│ Sesiones activas: 3 │ -│ - Chrome (Windows) - Hace 2 horas │ -│ - Firefox (Mac) - Hace 1 dia │ -│ - Safari (iPhone) - Ahora │ -│ │ -│ [ Cancelar ] [ Cerrar Todas ] │ -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API - -```typescript -// Logout individual -POST /api/v1/auth/logout -Authorization: Bearer eyJhbGciOiJSUzI1NiIs... -Cookie: refresh_token=eyJhbGciOiJSUzI1NiIs... - -// Response 200 -{ - "message": "Sesion cerrada exitosamente" -} -// Set-Cookie: refresh_token=; Max-Age=0; HttpOnly; Secure; SameSite=Strict - -// Logout all -POST /api/v1/auth/logout-all -Authorization: Bearer eyJhbGciOiJSUzI1NiIs... - -// Response 200 -{ - "message": "Todas las sesiones han sido cerradas", - "sessionsRevoked": 3 -} -``` - -### Flujo de Logout - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Frontend │ │ Backend │ │ Redis │ -└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ - │ │ │ - │ POST /logout │ │ - │───────────────────>│ │ - │ │ │ - │ │ Revoke refresh │ - │ │ token in DB │ - │ │ │ - │ │ Blacklist access │ - │ │───────────────────>│ - │ │ │ - │ │ Delete cookie │ - │ │ │ - │ 200 OK │ │ - │<───────────────────│ │ - │ │ │ - │ Clear local state │ │ - │ Redirect to login │ │ - │ │ │ -``` - -### Seguridad - -- Blacklist en Redis con TTL -- Logout idempotente (no falla si ya esta logged out) -- Rate limiting: 10 requests/minuto - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/auth/logout implementado -- [ ] Endpoint POST /api/v1/auth/logout-all implementado -- [ ] Refresh token revocado en BD -- [ ] Access token blacklisteado en Redis -- [ ] Cookie eliminada correctamente -- [ ] Registro en session_history -- [ ] Frontend limpia estado local -- [ ] Frontend redirige a login -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN001-001 | Login (sesion activa) | -| BlacklistService | Para invalidar tokens | -| Redis | Para almacenar blacklist | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| - | No bloquea otras historias | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: logout() | 2h | -| Backend: logoutAll() | 2h | -| Backend: Tests | 2h | -| Frontend: Logout button | 1h | -| Frontend: Confirmation modal | 2h | -| Frontend: Tests | 1h | -| **Total** | **10h** | - ---- - -## Referencias - -- [RF-AUTH-004](../../01-requerimientos/RF-auth/RF-AUTH-004.md) - Requerimiento funcional -- [ET-auth-backend](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Spec tecnica - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-003.md b/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-003.md deleted file mode 100644 index cc8c917..0000000 --- a/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-003.md +++ /dev/null @@ -1,300 +0,0 @@ -# US-MGN001-003: Renovacion Automatica de Tokens - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN001-003 | -| **Modulo** | MGN-001 Auth | -| **Sprint** | Sprint 1 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema ERP -**Quiero** que mi sesion se renueve automaticamente mientras estoy usando el sistema -**Para** no tener que re-autenticarme constantemente y tener una experiencia de usuario fluida - ---- - -## Descripcion - -El sistema debe renovar automaticamente los tokens de acceso antes de que expiren, utilizando el refresh token. Esto permite mantener sesiones de larga duracion (hasta 7 dias) mientras los access tokens mantienen una vida corta (15 minutos) por seguridad. - -### Contexto - -- Access tokens expiran en 15 minutos por seguridad -- Los usuarios no deben ser interrumpidos mientras trabajan -- El proceso debe ser transparente para el usuario - ---- - -## Criterios de Aceptacion - -### Escenario 1: Refresh automatico exitoso - -```gherkin -Given un usuario autenticado trabajando en el sistema - And su access token expira en menos de 1 minuto - And su refresh token es valido -When el frontend detecta que el token esta por expirar -Then el frontend envia automaticamente POST /api/v1/auth/refresh - And recibe nuevos access y refresh tokens - And actualiza los tokens en memoria - And la cookie httpOnly es actualizada - And el usuario NO es interrumpido -``` - -### Escenario 2: Refresh con token valido (manual) - -```gherkin -Given un usuario con refresh token valido -When hace POST /api/v1/auth/refresh -Then el sistema valida el refresh token - And genera un nuevo par de tokens - And el refresh token anterior es marcado como usado - And el nuevo refresh token pertenece a la misma familia - And responde con status 200 -``` - -### Escenario 3: Refresh token expirado - -```gherkin -Given un usuario con refresh token expirado (>7 dias) -When intenta renovar tokens -Then el sistema responde con status 401 - And el mensaje es "Refresh token expirado" - And el usuario es redirigido al login -``` - -### Escenario 4: Deteccion de token replay - -```gherkin -Given un refresh token que ya fue usado para renovar - And el sistema genero un nuevo token despues de usarlo -When alguien intenta usar el refresh token viejo -Then el sistema detecta el reuso - And invalida TODA la familia de tokens - And responde con status 401 - And el mensaje es "Sesion comprometida. Por favor inicia sesion." - And todas las sesiones del usuario son cerradas -``` - -### Escenario 5: Refresh sin token - -```gherkin -Given un request sin refresh token -When intenta renovar tokens -Then el sistema responde con status 400 - And el mensaje es "Refresh token requerido" -``` - -### Escenario 6: Multiples requests simultaneos - -```gherkin -Given un usuario con token por expirar - And el frontend hace 3 requests simultaneos de refresh -When los requests llegan al servidor -Then solo el primero obtiene nuevos tokens - And los siguientes reciben los nuevos tokens (idempotente) - OR los siguientes reciben error y deben reintentar con el nuevo token -``` - ---- - -## Mockup / Wireframe - -``` -El proceso es INVISIBLE para el usuario. No hay UI directa. - -┌──────────────────────────────────────────────────────────────────┐ -│ Indicador de sesion (opcional en header) │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ [🔒 Sesion activa] ← Verde: sesion renovada │ -│ │ -│ [⚠️ Renovando...] ← Amarillo: refresh en progreso │ -│ │ -│ [❌ Sesion expirada] ← Rojo: necesita login │ -│ │ -└──────────────────────────────────────────────────────────────────┘ - -Flujo de sesion expirada: -┌──────────────────────────────────────────────────────────────────┐ -│ SESION EXPIRADA │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ Tu sesion ha expirado. Por favor inicia sesion nuevamente. │ -│ │ -│ [ Ir a Login ] │ -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API - -```typescript -// Request -POST /api/v1/auth/refresh -Cookie: refresh_token=eyJhbGciOiJSUzI1NiIs... - -// O en body (fallback) -{ - "refreshToken": "eyJhbGciOiJSUzI1NiIs..." -} - -// Response 200 -{ - "accessToken": "eyJhbGciOiJSUzI1NiIs...", - "refreshToken": "eyJhbGciOiJSUzI1NiIs...", - "tokenType": "Bearer", - "expiresIn": 900 -} -// Set-Cookie: refresh_token=nuevo...; HttpOnly; Secure; SameSite=Strict -``` - -### Logica de Refresh en Frontend - -```typescript -// Token refresh interceptor (pseudo-code) -class TokenRefreshService { - private refreshing = false; - private refreshPromise: Promise | null = null; - - async checkAndRefresh(): Promise { - const accessToken = this.getAccessToken(); - const expiresAt = this.decodeExpiration(accessToken); - const now = Date.now(); - const oneMinute = 60 * 1000; - - // Renovar si expira en menos de 1 minuto - if (expiresAt - now < oneMinute) { - await this.refresh(); - } - } - - async refresh(): Promise { - // Evitar multiples refreshes simultaneos - if (this.refreshing) { - return this.refreshPromise; - } - - this.refreshing = true; - this.refreshPromise = this.doRefresh(); - - try { - await this.refreshPromise; - } finally { - this.refreshing = false; - this.refreshPromise = null; - } - } - - private async doRefresh(): Promise { - try { - const response = await api.post('/auth/refresh'); - this.setTokens(response.data); - } catch (error) { - // Refresh failed, redirect to login - this.clearTokens(); - router.push('/login'); - throw error; - } - } -} -``` - -### Rotacion de Tokens (Token Family) - -``` -Login exitoso: - └── RT1 (family: ABC) ← Activo - -Refresh 1: - ├── RT1 (family: ABC) → isUsed: true, replacedBy: RT2 - └── RT2 (family: ABC) ← Activo - -Refresh 2: - ├── RT1 (family: ABC) → isUsed: true, replacedBy: RT2 - ├── RT2 (family: ABC) → isUsed: true, replacedBy: RT3 - └── RT3 (family: ABC) ← Activo - -Token Replay (RT1 reutilizado): - ├── RT1 (family: ABC) → ALERTA! ya esta usado - ├── RT2 (family: ABC) → REVOCADO por seguridad - └── RT3 (family: ABC) → REVOCADO por seguridad -``` - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/auth/refresh implementado -- [ ] Rotacion de tokens funcionando -- [ ] Deteccion de token replay -- [ ] Revocacion de familia en caso de reuso -- [ ] Cookie actualizada en cada refresh -- [ ] Frontend: interceptor de refresh automatico -- [ ] Frontend: manejo de sesion expirada -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN001-001 | Login (obtener tokens iniciales) | -| Tabla refresh_tokens | Con campos family_id, is_used, replaced_by | -| Redis | Para rate limiting | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Todas las features | Dependen de sesion activa | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: refreshTokens() | 4h | -| Backend: Token rotation logic | 3h | -| Backend: Token replay detection | 2h | -| Backend: Tests | 3h | -| Frontend: Refresh interceptor | 3h | -| Frontend: Token storage | 2h | -| Frontend: Tests | 2h | -| **Total** | **19h** | - ---- - -## Referencias - -- [RF-AUTH-003](../../01-requerimientos/RF-auth/RF-AUTH-003.md) - Requerimiento funcional -- [RF-AUTH-002](../../01-requerimientos/RF-auth/RF-AUTH-002.md) - Estructura de tokens -- [ET-auth-backend](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Spec tecnica - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-004.md b/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-004.md deleted file mode 100644 index f446c5b..0000000 --- a/docs/01-fase-foundation/MGN-001-auth/historias-usuario/US-MGN001-004.md +++ /dev/null @@ -1,391 +0,0 @@ -# US-MGN001-004: Recuperacion de Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN001-004 | -| **Modulo** | MGN-001 Auth | -| **Sprint** | Sprint 2 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario que olvido su contraseña -**Quiero** poder recuperar el acceso a mi cuenta mediante un proceso seguro -**Para** no quedar bloqueado del sistema sin necesidad de contactar soporte - ---- - -## Descripcion - -El usuario que olvido su contraseña debe poder solicitar un enlace de recuperacion por email. Este enlace le permitira establecer una nueva contraseña de forma segura, invalidando el acceso anterior. - -### Contexto - -- Los usuarios olvidan contraseñas con frecuencia -- El proceso debe ser autoservicio -- Debe ser seguro contra ataques de enumeracion de emails - ---- - -## Criterios de Aceptacion - -### Escenario 1: Solicitud de recuperacion exitosa - -```gherkin -Given un usuario registrado con email "user@example.com" -When solicita recuperacion de password en POST /api/v1/auth/password/request-reset -Then el sistema responde con status 200 - And el mensaje es "Si el email esta registrado, recibiras instrucciones" - And se genera un token de recuperacion con expiracion de 1 hora - And se envia email con enlace de recuperacion - And tokens anteriores de ese usuario son invalidados -``` - -### Escenario 2: Solicitud para email inexistente (seguridad) - -```gherkin -Given un email "noexiste@example.com" que NO existe en el sistema -When solicita recuperacion de password -Then el sistema responde con status 200 - And el mensaje es IDENTICO al caso exitoso - And NO se revela que el email no existe - And NO se envia ningun email -``` - -### Escenario 3: Cambio de password exitoso - -```gherkin -Given un usuario con token de recuperacion valido - And el token no ha expirado (<1 hora) - And el token no ha sido usado -When envia nuevo password cumpliendo requisitos -Then el sistema actualiza el password hasheado - And invalida el token de recuperacion - And cierra TODAS las sesiones activas del usuario - And envia email confirmando el cambio - And responde con status 200 -``` - -### Escenario 4: Token de recuperacion expirado - -```gherkin -Given un token de recuperacion emitido hace mas de 1 hora -When el usuario intenta usarlo -Then el sistema responde con status 400 - And el mensaje es "Token de recuperacion expirado" -``` - -### Escenario 5: Token ya utilizado - -```gherkin -Given un token de recuperacion que ya fue usado -When el usuario intenta usarlo nuevamente -Then el sistema responde con status 400 - And el mensaje es "Token de recuperacion ya utilizado" -``` - -### Escenario 6: Password no cumple requisitos - -```gherkin -Given un token de recuperacion valido -When el usuario envia un password que no cumple requisitos - | Escenario | Password | Error | - | Muy corto | "abc123" | "Password debe tener minimo 8 caracteres" | - | Sin mayuscula | "password123!" | "Debe incluir una mayuscula" | - | Sin numero | "Password!!" | "Debe incluir un numero" | - | Sin especial | "Password123" | "Debe incluir caracter especial" | -Then el sistema responde con status 400 - And el mensaje indica el requisito faltante - And se incrementa el contador de intentos del token -``` - -### Escenario 7: Password igual a anterior - -```gherkin -Given un token de recuperacion valido - And el usuario tiene historial de passwords -When envia un password igual a uno de los ultimos 5 -Then el sistema responde con status 400 - And el mensaje es "No puedes usar una contraseña anterior" -``` - -### Escenario 8: Validacion de token - -```gherkin -Given un token de recuperacion -When el usuario navega a la pagina de reset con ese token -Then el frontend valida el token GET /api/v1/auth/password/validate-token/:token - And si es valido muestra el formulario - And si no es valido muestra mensaje de error apropiado -``` - ---- - -## Mockup / Wireframe - -### Pagina de Solicitud - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| ╔═══════════════════════════════════╗ | -| ║ RECUPERAR CONTRASEÑA ║ | -| ╚═══════════════════════════════════╝ | -| | -| Ingresa tu correo electronico y te enviaremos | -| instrucciones para restablecer tu contraseña. | -| | -| Correo electronico | -| +---------------------------+ | -| | user@example.com | | -| +---------------------------+ | -| | -| [===== ENVIAR INSTRUCCIONES =====] | -| | -| ← Volver al login | -| | -+------------------------------------------------------------------+ -``` - -### Confirmacion de Envio - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| ╔═══════════════════════════════════╗ | -| ║ EMAIL ENVIADO ✉️ ║ | -| ╚═══════════════════════════════════╝ | -| | -| Si el email esta registrado, recibiras | -| instrucciones en los proximos minutos. | -| | -| Revisa tu bandeja de entrada y la carpeta | -| de spam. | -| | -| [===== VOLVER AL LOGIN =====] | -| | -| ¿No recibiste el email? | -| Espera 5 minutos y vuelve a intentar. | -| | -+------------------------------------------------------------------+ -``` - -### Pagina de Reset - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| ╔═══════════════════════════════════╗ | -| ║ NUEVA CONTRASEÑA ║ | -| ╚═══════════════════════════════════╝ | -| | -| Nueva contraseña | -| +---------------------------+ | -| | •••••••••••• | [👁] | -| +---------------------------+ | -| [████████░░] Fuerte | -| | -| ✓ Minimo 8 caracteres | -| ✓ Al menos una mayuscula | -| ✗ Al menos un numero | -| ✗ Al menos un caracter especial | -| | -| Confirmar contraseña | -| +---------------------------+ | -| | •••••••••••• | [👁] | -| +---------------------------+ | -| | -| [===== CAMBIAR CONTRASEÑA =====] | -| | -+------------------------------------------------------------------+ -``` - -### Token Invalido - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| ╔═══════════════════════════════════╗ | -| ║ ⚠️ ENLACE INVALIDO ║ | -| ╚═══════════════════════════════════╝ | -| | -| El enlace de recuperacion ha expirado | -| o ya fue utilizado. | -| | -| [===== SOLICITAR NUEVO ENLACE =====] | -| | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API - -```typescript -// 1. Solicitar recuperacion -POST /api/v1/auth/password/request-reset -{ - "email": "user@example.com" -} -// Response 200 -{ - "message": "Si el email esta registrado, recibiras instrucciones" -} - -// 2. Validar token -GET /api/v1/auth/password/validate-token/a1b2c3d4e5f6... -// Response 200 (valido) -{ - "valid": true, - "email": "u***@example.com" -} -// Response 200 (invalido) -{ - "valid": false, - "reason": "expired" | "used" | "invalid" -} - -// 3. Cambiar password -POST /api/v1/auth/password/reset -{ - "token": "a1b2c3d4e5f6...", - "newPassword": "NewSecurePass123!", - "confirmPassword": "NewSecurePass123!" -} -// Response 200 -{ - "message": "Contraseña actualizada exitosamente" -} -``` - -### Template de Email - -```html -Asunto: Recuperacion de contraseña - ERP Suite - -

Hola {{firstName}},

- -

Recibimos una solicitud para restablecer tu contraseña.

- -

Haz clic en el siguiente enlace para crear una nueva contraseña:

- -Restablecer Contraseña - -

Este enlace expira en 1 hora.

- -

Si no solicitaste este cambio, ignora este email. Tu contraseña -permanecera sin cambios.

- -

Por seguridad, nunca compartas este enlace con nadie.

- -
-IP: {{ipAddress}} | Fecha: {{timestamp}} -``` - -### Validaciones de Password - -```typescript -const PASSWORD_RULES = { - minLength: 8, - maxLength: 128, - requireUppercase: true, - requireLowercase: true, - requireNumber: true, - requireSpecial: true, - specialChars: '!@#$%^&*()_+-=[]{}|;:,.<>?', - historyCount: 5, // No repetir ultimos 5 -}; - -const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; -``` - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/auth/password/request-reset implementado -- [ ] Endpoint GET /api/v1/auth/password/validate-token/:token implementado -- [ ] Endpoint POST /api/v1/auth/password/reset implementado -- [ ] Token de 256 bits generado con crypto.randomBytes -- [ ] Token hasheado antes de almacenar -- [ ] Email de recuperacion enviado -- [ ] Validacion de politica de password -- [ ] Historial de passwords implementado -- [ ] Logout-all despues de cambio -- [ ] Email de confirmacion enviado -- [ ] Frontend: ForgotPasswordPage -- [ ] Frontend: ResetPasswordPage -- [ ] Frontend: Password strength indicator -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| EmailService | Para enviar emails | -| Tabla password_reset_tokens | Almacenar tokens | -| Tabla password_history | Historial de passwords | -| US-MGN001-002 | Logout-all functionality | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| - | No bloquea otras historias | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: requestPasswordReset() | 3h | -| Backend: validateResetToken() | 2h | -| Backend: resetPassword() | 4h | -| Backend: Password validation | 2h | -| Backend: Email templates | 2h | -| Backend: Tests | 3h | -| Frontend: ForgotPasswordPage | 3h | -| Frontend: ResetPasswordPage | 4h | -| Frontend: Password strength | 2h | -| Frontend: Tests | 2h | -| **Total** | **27h** | - ---- - -## Referencias - -- [RF-AUTH-005](../../01-requerimientos/RF-auth/RF-AUTH-005.md) - Requerimiento funcional -- [ET-auth-backend](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Spec tecnica - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-001.md b/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-001.md deleted file mode 100644 index b446f8b..0000000 --- a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-001.md +++ /dev/null @@ -1,219 +0,0 @@ -# US-MGN002-001: CRUD de Usuarios (Admin) - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-001 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** poder crear, ver, editar y eliminar usuarios -**Para** gestionar el acceso de los empleados a la plataforma ERP - ---- - -## Criterios de Aceptacion - -### Escenario 1: Crear usuario exitosamente - -```gherkin -Given un administrador autenticado con permiso "users:create" -When crea un usuario con: - | email | nuevo@empresa.com | - | firstName | Juan | - | lastName | Perez | - | roleIds | [admin-role-id] | -Then el sistema crea el usuario con status "pending_activation" - And envia email de invitacion al usuario - And responde con status 201 - And el body contiene el usuario creado sin password -``` - -### Escenario 2: Listar usuarios con paginacion - -```gherkin -Given 50 usuarios en el tenant actual -When el admin solicita GET /api/v1/users?page=2&limit=10 -Then el sistema retorna usuarios 11-20 - And incluye meta con total, pages, hasNext, hasPrev -``` - -### Escenario 3: Buscar usuarios por texto - -```gherkin -Given usuarios con nombres "Juan", "Juana", "Pedro" -When el admin busca con search="juan" -Then el sistema retorna "Juan" y "Juana" - And no retorna "Pedro" -``` - -### Escenario 4: Soft delete de usuario - -```gherkin -Given un usuario activo con id "user-123" -When el admin elimina el usuario -Then el campo deleted_at se establece - And el campo deleted_by tiene el ID del admin - And el usuario no aparece en listados - And el usuario no puede hacer login -``` - -### Escenario 5: No puede eliminarse a si mismo - -```gherkin -Given un admin con id "admin-123" -When intenta eliminar su propio usuario -Then el sistema responde con status 400 - And el mensaje es "No puedes eliminarte a ti mismo" -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Usuarios [+ Nuevo Usuario] | -+------------------------------------------------------------------+ -| Buscar: [___________________] [Filtros ▼] | -+------------------------------------------------------------------+ -| ☐ | Avatar | Nombre | Email | Estado | Roles | -|---|--------|---------------|--------------------|---------|----- | -| ☐ | 👤 | Juan Perez | juan@empresa.com | Activo | Admin| -| ☐ | 👤 | Maria Lopez | maria@empresa.com | Activo | User | -| ☐ | 👤 | Pedro Garcia | pedro@empresa.com | Inactivo| User | -+------------------------------------------------------------------+ -| Mostrando 1-10 de 50 [< Anterior] [Siguiente >]| -+------------------------------------------------------------------+ - -Modal: Crear Usuario -┌──────────────────────────────────────────────────────────────────┐ -│ NUEVO USUARIO │ -├──────────────────────────────────────────────────────────────────┤ -│ Email* [_______________________________] │ -│ Nombre* [_______________________________] │ -│ Apellido* [_______________________________] │ -│ Telefono [_______________________________] │ -│ Roles [Select roles... ▼] │ -│ ☑ Admin ☐ Manager ☐ User │ -│ │ -│ [ Cancelar ] [ Crear Usuario ] │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Crear usuario -POST /api/v1/users -{ - "email": "nuevo@empresa.com", - "firstName": "Juan", - "lastName": "Perez", - "phone": "+521234567890", - "roleIds": ["role-uuid"] -} - -// Response 201 -{ - "id": "user-uuid", - "email": "nuevo@empresa.com", - "firstName": "Juan", - "lastName": "Perez", - "status": "pending_activation", - "isActive": false, - "createdAt": "2025-12-05T10:00:00Z", - "roles": [{ "id": "role-uuid", "name": "admin" }] -} - -// Listar usuarios -GET /api/v1/users?page=1&limit=20&search=juan&status=active&sortBy=createdAt&sortOrder=DESC - -// Response 200 -{ - "data": [...], - "meta": { - "total": 50, - "page": 1, - "limit": 20, - "totalPages": 3, - "hasNext": true, - "hasPrev": false - } -} -``` - -### Permisos Requeridos - -| Accion | Permiso | -|--------|---------| -| Crear | users:create | -| Listar | users:read | -| Ver detalle | users:read | -| Actualizar | users:update | -| Eliminar | users:delete | -| Activar/Desactivar | users:update | - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/users implementado -- [ ] Endpoint GET /api/v1/users con paginacion y filtros -- [ ] Endpoint GET /api/v1/users/:id -- [ ] Endpoint PATCH /api/v1/users/:id -- [ ] Endpoint DELETE /api/v1/users/:id (soft delete) -- [ ] Validaciones de permisos (RBAC) -- [ ] Email de invitacion enviado -- [ ] Frontend: UsersListPage con tabla -- [ ] Frontend: Modal crear/editar usuario -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-AUTH-001 | Login para autenticacion | -| RF-ROLE-001 | Roles para asignacion | -| EmailService | Para enviar invitaciones | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: CRUD endpoints | 6h | -| Backend: Paginacion y filtros | 3h | -| Backend: Tests | 3h | -| Frontend: UsersListPage | 4h | -| Frontend: UserForm modal | 3h | -| Frontend: Tests | 2h | -| **Total** | **21h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-002.md b/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-002.md deleted file mode 100644 index d880c16..0000000 --- a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-002.md +++ /dev/null @@ -1,225 +0,0 @@ -# US-MGN002-002: Perfil de Usuario - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-002 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 2 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 5 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema -**Quiero** poder ver y editar mi informacion personal -**Para** mantener mis datos actualizados y personalizar mi experiencia - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver mi perfil - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/users/me -Then el sistema retorna su perfil completo - And incluye firstName, lastName, email, phone - And incluye avatarUrl, avatarThumbnailUrl - And incluye createdAt, lastLoginAt - And incluye sus roles asignados - And NO incluye passwordHash -``` - -### Escenario 2: Actualizar nombre - -```gherkin -Given un usuario autenticado con nombre "Juan" -When actualiza su nombre a "Carlos" -Then el sistema guarda el cambio - And responde con el perfil actualizado - And firstName es "Carlos" -``` - -### Escenario 3: Subir avatar - -```gherkin -Given un usuario autenticado - And una imagen JPG de 2MB -When sube la imagen como avatar -Then el sistema redimensiona a 200x200 px - And genera thumbnail de 50x50 px - And actualiza avatarUrl en su perfil - And responde con las URLs generadas -``` - -### Escenario 4: Avatar muy grande - -```gherkin -Given un usuario autenticado - And una imagen de 15MB -When intenta subir como avatar -Then el sistema responde con status 400 - And el mensaje es "Imagen excede tamaño maximo (10MB)" -``` - -### Escenario 5: Eliminar avatar - -```gherkin -Given un usuario con avatar -When elimina su avatar -Then avatarUrl se establece a null - And avatarThumbnailUrl se establece a null - And el avatar anterior se marca como no actual -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Mi Perfil | -+------------------------------------------------------------------+ -| | -| +-------+ | -| | | Juan Perez | -| | FOTO | juan@empresa.com | -| | | Admin, Manager | -| +-------+ | -| [Cambiar foto] | -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ INFORMACION PERSONAL │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Nombre [Juan ] │ | -| │ Apellido [Perez ] │ | -| │ Telefono [+521234567890 ] │ | -| │ Email juan@empresa.com [Cambiar email] │ | -| │ │ | -| │ [ Cancelar ] [ Guardar Cambios ] │ | -| └─────────────────────────────────────────────────────────┘ | -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ SEGURIDAD │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Contraseña •••••••• [Cambiar contraseña] │ | -| │ Ultimo login 05/12/2025 10:30 │ | -| │ Miembro desde 01/01/2025 │ | -| └─────────────────────────────────────────────────────────┘ | -+------------------------------------------------------------------+ - -Modal: Subir Avatar -┌──────────────────────────────────────────────────────────────────┐ -│ CAMBIAR FOTO │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ +------------------+ │ -│ | | Arrastra una imagen o │ -│ | [ + ] | [Selecciona archivo] │ -│ | | │ -│ +------------------+ Formatos: JPG, PNG, WebP │ -│ Tamaño max: 10MB │ -│ │ -│ [ Cancelar ] [ Subir ] │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Obtener perfil -GET /api/v1/users/me - -// Response 200 -{ - "id": "user-uuid", - "email": "juan@empresa.com", - "firstName": "Juan", - "lastName": "Perez", - "phone": "+521234567890", - "avatarUrl": "https://storage.../avatar-200.jpg", - "avatarThumbnailUrl": "https://storage.../avatar-50.jpg", - "status": "active", - "emailVerifiedAt": "2025-01-01T00:00:00Z", - "lastLoginAt": "2025-12-05T10:30:00Z", - "createdAt": "2025-01-01T00:00:00Z", - "roles": [{ "id": "...", "name": "admin" }] -} - -// Actualizar perfil -PATCH /api/v1/users/me -{ - "firstName": "Carlos", - "lastName": "Lopez", - "phone": "+521234567890" -} - -// Subir avatar -POST /api/v1/users/me/avatar -Content-Type: multipart/form-data -avatar: [file] - -// Response 200 -{ - "avatarUrl": "https://storage.../avatar-200.jpg", - "avatarThumbnailUrl": "https://storage.../avatar-50.jpg" -} -``` - -### Validaciones de Avatar - -| Validacion | Valor | -|------------|-------| -| Formatos | image/jpeg, image/png, image/webp | -| Tamaño max | 10MB | -| Resize main | 200x200 px | -| Resize thumb | 50x50 px | - ---- - -## Definicion de Done - -- [ ] Endpoint GET /api/v1/users/me -- [ ] Endpoint PATCH /api/v1/users/me -- [ ] Endpoint POST /api/v1/users/me/avatar -- [ ] Endpoint DELETE /api/v1/users/me/avatar -- [ ] Procesamiento de imagen con Sharp -- [ ] Upload a storage (S3/local) -- [ ] Frontend: ProfilePage -- [ ] Frontend: AvatarUploader component -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Profile endpoints | 3h | -| Backend: Avatar upload + resize | 4h | -| Backend: Tests | 2h | -| Frontend: ProfilePage | 4h | -| Frontend: AvatarUploader | 3h | -| Frontend: Tests | 2h | -| **Total** | **18h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-003.md b/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-003.md deleted file mode 100644 index 5ce1d05..0000000 --- a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-003.md +++ /dev/null @@ -1,203 +0,0 @@ -# US-MGN002-003: Cambio de Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-003 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 3 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema -**Quiero** poder cambiar mi contraseña -**Para** mantener mi cuenta segura y cumplir con politicas de rotacion - ---- - -## Criterios de Aceptacion - -### Escenario 1: Cambio exitoso - -```gherkin -Given un usuario autenticado -When proporciona password actual correcto "OldPass123!" - And nuevo password "NewPass456!" cumple requisitos -Then el sistema actualiza el password - And guarda hash en password_history - And envia email de notificacion - And responde con status 200 -``` - -### Escenario 2: Password actual incorrecto - -```gherkin -Given un usuario autenticado -When proporciona password actual incorrecto -Then el sistema responde con status 400 - And el mensaje es "Password actual incorrecto" -``` - -### Escenario 3: Nuevo password no cumple requisitos - -```gherkin -Given un usuario autenticado -When el nuevo password es "abc123" -Then el sistema responde con status 400 - And lista los requisitos no cumplidos: - | Debe tener al menos 8 caracteres | - | Debe incluir una mayuscula | - | Debe incluir caracter especial | -``` - -### Escenario 4: Password reutilizado - -```gherkin -Given un usuario que uso "MiPass123!" hace 2 meses -When intenta cambiar a "MiPass123!" -Then el sistema responde con status 400 - And el mensaje es "No puedes usar un password que hayas usado anteriormente" -``` - -### Escenario 5: Cerrar otras sesiones - -```gherkin -Given un usuario con 3 sesiones activas -When cambia password con logoutOtherSessions=true -Then el sistema actualiza el password - And invalida las otras 2 sesiones - And la sesion actual permanece activa - And responde con sessionsInvalidated: 2 -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Cambiar Contraseña | -+------------------------------------------------------------------+ -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ CAMBIAR CONTRASEÑA │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ │ | -| │ Contraseña actual │ | -| │ [••••••••••••••••••• ] 👁 │ | -| │ │ | -| │ Nueva contraseña │ | -| │ [••••••••••••••••••• ] 👁 │ | -| │ [████████████░░░░░░░░] Fuerte │ | -| │ │ | -| │ ✓ Minimo 8 caracteres │ | -| │ ✓ Al menos una mayuscula │ | -| │ ✓ Al menos una minuscula │ | -| │ ✗ Al menos un numero │ | -| │ ✗ Al menos un caracter especial │ | -| │ │ | -| │ Confirmar nueva contraseña │ | -| │ [••••••••••••••••••• ] 👁 │ | -| │ │ | -| │ ☐ Cerrar sesion en otros dispositivos │ | -| │ │ | -| │ [ Cancelar ] [ Cambiar Contraseña ] │ | -| └─────────────────────────────────────────────────────────┘ | -| | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoint - -```typescript -POST /api/v1/users/me/password -{ - "currentPassword": "OldPass123!", - "newPassword": "NewPass456!", - "confirmPassword": "NewPass456!", - "logoutOtherSessions": true -} - -// Response 200 -{ - "message": "Password actualizado exitosamente", - "sessionsInvalidated": 2 -} - -// Response 400 - Password incorrecto -{ - "statusCode": 400, - "message": "Password actual incorrecto" -} - -// Response 400 - No cumple politica -{ - "statusCode": 400, - "message": "El password no cumple los requisitos", - "errors": [ - "Debe incluir al menos una mayuscula", - "Debe incluir al menos un caracter especial" - ] -} -``` - -### Politica de Password - -``` -- Minimo 8 caracteres -- Maximo 128 caracteres -- Al menos 1 mayuscula -- Al menos 1 minuscula -- Al menos 1 numero -- Al menos 1 caracter especial (!@#$%^&*) -- No puede contener el email -- No puede ser igual a los ultimos 5 -``` - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/users/me/password -- [ ] Validacion de password actual -- [ ] Validacion de politica de complejidad -- [ ] Verificacion contra historial (5 anteriores) -- [ ] Opcion de cerrar otras sesiones -- [ ] Email de notificacion enviado -- [ ] Frontend: ChangePasswordForm -- [ ] Frontend: PasswordStrengthIndicator -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Endpoint | 2h | -| Backend: Validaciones | 2h | -| Backend: Tests | 1h | -| Frontend: Form + indicator | 3h | -| Frontend: Tests | 1h | -| **Total** | **9h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-004.md b/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-004.md deleted file mode 100644 index 3408e03..0000000 --- a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-004.md +++ /dev/null @@ -1,222 +0,0 @@ -# US-MGN002-004: Preferencias de Usuario - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-004 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 3 | -| **Prioridad** | P2 - Media | -| **Story Points** | 5 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema -**Quiero** poder configurar mis preferencias personales -**Para** personalizar mi experiencia en la plataforma - ---- - -## Criterios de Aceptacion - -### Escenario 1: Obtener preferencias - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/users/me/preferences -Then el sistema retorna sus preferencias - And si no tiene preferencias, retorna defaults del tenant -``` - -### Escenario 2: Cambiar idioma - -```gherkin -Given un usuario con idioma "es" -When cambia a idioma "en" -Then el sistema guarda la preferencia - And la interfaz cambia a ingles inmediatamente -``` - -### Escenario 3: Cambiar tema - -```gherkin -Given un usuario con tema "light" -When activa tema "dark" -Then la interfaz cambia a colores oscuros - And la preferencia persiste entre sesiones -``` - -### Escenario 4: Configurar notificaciones - -```gherkin -Given un usuario con notificaciones de marketing activas -When desactiva notificaciones de marketing -Then deja de recibir ese tipo de emails - And mantiene otras notificaciones activas -``` - -### Escenario 5: Reset preferencias - -```gherkin -Given un usuario con preferencias personalizadas -When ejecuta reset de preferencias -Then todas sus preferencias vuelven a defaults del tenant -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Preferencias | -+------------------------------------------------------------------+ -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ IDIOMA Y REGION │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Idioma [Español ▼] │ | -| │ Zona horaria [America/Mexico_City ▼] │ | -| │ Formato fecha [DD/MM/YYYY ▼] │ | -| │ Formato hora [24 horas ▼] │ | -| │ Moneda [MXN - Peso Mexicano ▼] │ | -| └─────────────────────────────────────────────────────────┘ | -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ APARIENCIA │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Tema [☀️ Claro] [🌙 Oscuro] [💻 Sistema] │ | -| │ Tamaño fuente [Pequeño] [Mediano] [Grande] │ | -| │ ☐ Modo compacto │ | -| │ ☐ Sidebar colapsado por defecto │ | -| └─────────────────────────────────────────────────────────┘ | -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ NOTIFICACIONES │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Email │ | -| │ ☑ Habilitado Frecuencia: [Diario ▼] │ | -| │ ☑ Alertas de seguridad │ | -| │ ☑ Actualizaciones del sistema │ | -| │ ☐ Comunicaciones de marketing │ | -| │ │ | -| │ Push │ | -| │ ☑ Habilitado │ | -| │ ☑ Sonido de notificacion │ | -| │ │ | -| │ In-App │ | -| │ ☑ Habilitado │ | -| │ ☐ Notificaciones de escritorio │ | -| └─────────────────────────────────────────────────────────┘ | -| | -| [Restaurar valores predeterminados] [ Guardar Cambios ] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Obtener preferencias -GET /api/v1/users/me/preferences - -// Response 200 -{ - "language": "es", - "timezone": "America/Mexico_City", - "dateFormat": "DD/MM/YYYY", - "timeFormat": "24h", - "currency": "MXN", - "numberFormat": "es-MX", - "theme": "dark", - "sidebarCollapsed": false, - "compactMode": false, - "fontSize": "medium", - "notifications": { - "email": { - "enabled": true, - "digest": "daily", - "marketing": false, - "security": true, - "updates": true - }, - "push": { "enabled": true, "sound": true }, - "inApp": { "enabled": true, "desktop": false } - }, - "dashboard": { - "defaultView": "overview", - "widgets": ["sales", "inventory"] - } -} - -// Actualizar preferencias (parcial) -PATCH /api/v1/users/me/preferences -{ - "theme": "light", - "notifications": { - "email": { "marketing": false } - } -} - -// Reset preferencias -POST /api/v1/users/me/preferences/reset -``` - -### Aplicacion en Frontend - -```typescript -// Al cambiar tema -useEffect(() => { - document.documentElement.setAttribute('data-theme', preferences.theme); -}, [preferences.theme]); - -// Al cambiar idioma -useEffect(() => { - i18n.changeLanguage(preferences.language); -}, [preferences.language]); -``` - ---- - -## Definicion de Done - -- [ ] Endpoint GET /api/v1/users/me/preferences -- [ ] Endpoint PATCH /api/v1/users/me/preferences -- [ ] Endpoint POST /api/v1/users/me/preferences/reset -- [ ] Deep merge para actualizaciones parciales -- [ ] Frontend: PreferencesPage con todas las secciones -- [ ] Frontend: Aplicacion inmediata de cambios (tema, idioma) -- [ ] Frontend: PreferencesContext para estado global -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Endpoints | 3h | -| Backend: Deep merge logic | 1h | -| Backend: Tests | 2h | -| Frontend: PreferencesPage | 5h | -| Frontend: Theme/Language context | 3h | -| Frontend: Tests | 2h | -| **Total** | **16h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-005.md b/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-005.md deleted file mode 100644 index 18cb1a6..0000000 --- a/docs/01-fase-foundation/MGN-002-users/historias-usuario/US-MGN002-005.md +++ /dev/null @@ -1,286 +0,0 @@ -# US-MGN002-005: Cambio de Email - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-005 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 3 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 6 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-06 | -| **RF Asociado** | RF-USER-003 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema -**Quiero** poder cambiar mi direccion de email de forma segura -**Para** mantener mi cuenta actualizada y proteger el acceso a mi informacion - ---- - -## Descripcion - -El usuario necesita un proceso seguro para cambiar su email registrado. El sistema debe verificar la identidad del usuario con su password actual y validar la propiedad del nuevo email antes de aplicar el cambio. Esto protege contra cambios no autorizados. - -### Contexto - -- El email es el identificador principal de login -- Se usa para recuperacion de password y notificaciones -- Requiere doble verificacion por seguridad (password + email) -- Todas las sesiones se invalidan despues del cambio - ---- - -## Criterios de Aceptacion - -### Escenario 1: Solicitar cambio de email exitosamente - -```gherkin -Given un usuario autenticado con email "viejo@empresa.com" -When solicita cambiar a "nuevo@empresa.com" - And confirma con su password actual - And el nuevo email no esta registrado -Then el sistema genera token de verificacion - And envia email de verificacion a "nuevo@empresa.com" - And el email actual permanece sin cambios - And responde con status 200 - And el body contiene "expiresAt" con fecha +24h -``` - -### Escenario 2: Verificar nuevo email - -```gherkin -Given un usuario con solicitud de cambio pendiente - And un token de verificacion valido (< 24 horas) -When hace clic en enlace de verificacion -Then el sistema actualiza el email del usuario - And invalida todas las sesiones activas - And envia notificacion al email anterior - And redirige al login con mensaje de exito -``` - -### Escenario 3: Nuevo email ya registrado - -```gherkin -Given un email "existente@empresa.com" ya en uso -When un usuario intenta cambiar a ese email -Then el sistema responde con status 409 - And el mensaje es "Email no disponible" -``` - -### Escenario 4: Password incorrecto - -```gherkin -Given un usuario autenticado -When solicita cambio de email con password incorrecto -Then el sistema responde con status 400 - And el mensaje es "Password incorrecto" - And no se crea solicitud de cambio -``` - -### Escenario 5: Token de verificacion expirado - -```gherkin -Given un token de verificacion emitido hace mas de 24 horas -When el usuario intenta usarlo -Then el sistema responde con status 400 - And el mensaje es "Token expirado, solicita nuevo cambio" -``` - -### Escenario 6: Cancelar solicitud pendiente - -```gherkin -Given un usuario con cambio de email pendiente -When solicita un nuevo cambio -Then la solicitud anterior se invalida - And se crea nueva solicitud con nuevo token -``` - ---- - -## Mockup / Wireframe - -``` -Pagina: Mi Perfil - Seccion Email -┌──────────────────────────────────────────────────────────────────┐ -│ Email actual: juan@empresa.com │ -│ [Cambiar Email] │ -└──────────────────────────────────────────────────────────────────┘ - -Modal: Cambiar Email -┌──────────────────────────────────────────────────────────────────┐ -│ CAMBIAR EMAIL │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ Email actual juan@empresa.com (no editable) │ -│ │ -│ Nuevo email* [_______________________________] │ -│ │ -│ Confirmar password* [_______________________________] │ -│ │ -│ ⚠️ Se enviara un link de verificacion al nuevo email. │ -│ Tu email no cambiara hasta que verifiques el enlace. │ -│ Todas tus sesiones seran cerradas despues del cambio. │ -│ │ -│ [ Cancelar ] [ Enviar Verificacion ] │ -└──────────────────────────────────────────────────────────────────┘ - -Pagina: Verificacion Exitosa -┌──────────────────────────────────────────────────────────────────┐ -│ │ -│ ✅ Email Actualizado │ -│ │ -│ Tu email ha sido cambiado a: nuevo@empresa.com │ -│ │ -│ Por seguridad, todas tus sesiones han sido cerradas. │ -│ │ -│ [ Ir al Login ] │ -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Solicitar cambio de email -POST /api/v1/users/me/email/request-change -{ - "newEmail": "nuevo@empresa.com", - "currentPassword": "MiPasswordActual123!" -} - -// Response 200 -{ - "message": "Se ha enviado un email de verificacion a nuevo@empresa.com", - "expiresAt": "2025-12-07T10:30:00Z" -} - -// Response 400 - Password incorrecto -{ - "statusCode": 400, - "message": "Password incorrecto" -} - -// Response 409 - Email existe -{ - "statusCode": 409, - "message": "Email no disponible" -} - -// Verificar cambio de email -GET /api/v1/users/email/verify-change?token=abc123... - -// Response 200 (redirect) -// Redirect to: /login?emailChanged=true - -// Response 400 - Token invalido -{ - "statusCode": 400, - "message": "Token invalido o expirado" -} -``` - -### Permisos Requeridos - -| Accion | Permiso | -|--------|---------| -| Solicitar cambio | Autenticado (propio email) | -| Verificar cambio | Token valido | - -### Schema de Base de Datos - -```sql -CREATE TABLE core_users.email_change_requests ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES core_users.users(id), - tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id), - current_email VARCHAR(255) NOT NULL, - new_email VARCHAR(255) NOT NULL, - token_hash VARCHAR(255) NOT NULL, - expires_at TIMESTAMPTZ NOT NULL, - completed_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - CONSTRAINT uq_pending_email_change UNIQUE (user_id, completed_at) - WHERE completed_at IS NULL -); - -CREATE INDEX idx_email_change_user ON core_users.email_change_requests(user_id); -CREATE INDEX idx_email_change_token ON core_users.email_change_requests(token_hash); -``` - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/users/me/email/request-change implementado -- [ ] Endpoint GET /api/v1/users/email/verify-change implementado -- [ ] Tabla email_change_requests creada con RLS -- [ ] Validacion de password actual -- [ ] Validacion de email unico en tenant -- [ ] Generacion de token seguro (32 bytes hex) -- [ ] Envio de email de verificacion -- [ ] Envio de notificacion al email anterior -- [ ] Invalidacion de sesiones post-cambio -- [ ] Frontend: ChangeEmailForm integrado en perfil -- [ ] Frontend: Pagina de verificacion exitosa -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-USER-001 | CRUD Usuarios (tabla users) | -| RF-AUTH-001 | Login para autenticacion | -| RF-AUTH-004 | Logout para invalidar sesiones | -| EmailService | Para enviar verificacion | - ---- - -## Estimacion - -| Tarea | Story Points | -|-------|--------------| -| Database: Tabla email_change_requests | 1 | -| Backend: Endpoint request-change | 1.5 | -| Backend: Endpoint verify-change | 1.5 | -| Backend: Tests | 1 | -| Frontend: ChangeEmailForm | 0.5 | -| Frontend: Pagina verificacion | 0.5 | -| **Total** | **6** | - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | Requiere password actual | Verificacion bcrypt | -| RN-002 | Nuevo email unico en tenant | UNIQUE constraint | -| RN-003 | Token expira en 24 horas | expires_at check | -| RN-004 | Una solicitud activa a la vez | Invalidar anteriores | -| RN-005 | Notificar email anterior | Email de seguridad | -| RN-006 | Logout-all post-cambio | Revocar tokens | -| RN-007 | Formato email valido | Regex validation | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-06 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-001.md b/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-001.md deleted file mode 100644 index 72cbe99..0000000 --- a/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-001.md +++ /dev/null @@ -1,162 +0,0 @@ -# US-MGN003-001: CRUD de Roles - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN003-001 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** poder crear, ver, editar y eliminar roles -**Para** gestionar los niveles de acceso de los usuarios a las funcionalidades del sistema - ---- - -## Criterios de Aceptacion - -### Escenario 1: Crear rol exitosamente - -```gherkin -Given un admin autenticado con permiso "roles:create" -When crea un rol con: - | name | Vendedor | - | description | Equipo de ventas | - | permissions | [sales:*, users:read] | -Then el sistema crea el rol - And el rol tiene slug "vendedor" - And el rol tiene isBuiltIn = false - And responde con status 201 -``` - -### Escenario 2: Listar roles con paginacion - -```gherkin -Given 12 roles en el tenant (5 built-in + 7 custom) -When el admin solicita GET /api/v1/roles?page=1&limit=10 -Then el sistema retorna 10 roles - And roles built-in aparecen primero - And incluye usersCount y permissionsCount por rol - And incluye meta con total=12 -``` - -### Escenario 3: No crear rol con nombre duplicado - -```gherkin -Given un rol existente con nombre "Vendedor" -When el admin intenta crear otro rol "Vendedor" -Then el sistema responde con status 409 - And el mensaje es "Ya existe un rol con este nombre" -``` - -### Escenario 4: No eliminar rol del sistema - -```gherkin -Given el rol built-in "admin" -When el admin intenta eliminarlo -Then el sistema responde con status 400 - And el mensaje es "No se pueden eliminar roles del sistema" -``` - -### Escenario 5: Soft delete con reasignacion - -```gherkin -Given rol "Vendedor" con 5 usuarios asignados -When admin elimina el rol con reassignTo="user" -Then el rol tiene deleted_at establecido - And los 5 usuarios ahora tienen rol "user" - And el rol no aparece en listados normales -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Roles [+ Nuevo Rol] | -+------------------------------------------------------------------+ -| Buscar: [___________________] [Tipo: Todos ▼] | -+------------------------------------------------------------------+ -| | Nombre | Descripcion | Permisos | Usuarios | ⚙ | -|---|-----------------|--------------------|-----------|---------|----| -| 🔒| Super Admin | Acceso total | 45 | 1 | 👁 | -| 🔒| Admin | Gestion tenant | 32 | 3 | 👁 | -| 🔒| Manager | Supervision | 18 | 5 | 👁 | -| 🔒| User | Acceso basico | 8 | 42 | 👁 | -| | Vendedor | Equipo de ventas | 12 | 8 | ✏🗑| -| | Contador | Area contable | 15 | 2 | ✏🗑| -+------------------------------------------------------------------+ - -🔒 = Rol del sistema (built-in), no editable/eliminable -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -POST /api/v1/roles -GET /api/v1/roles?page=1&limit=10&type=custom&search=vend -GET /api/v1/roles/:id -PATCH /api/v1/roles/:id -DELETE /api/v1/roles/:id?reassignTo=other-role-id -``` - -### Validaciones - -| Campo | Regla | -|-------|-------| -| name | 3-50 chars, unico en tenant | -| description | Max 500 chars | -| permissionIds | Array min 1 | - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/roles -- [ ] Endpoint GET /api/v1/roles con paginacion -- [ ] Endpoint GET /api/v1/roles/:id -- [ ] Endpoint PATCH /api/v1/roles/:id -- [ ] Endpoint DELETE /api/v1/roles/:id -- [ ] Validacion de roles built-in -- [ ] Frontend: RolesListPage -- [ ] Frontend: CreateRoleModal -- [ ] Frontend: EditRoleModal -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: CRUD endpoints | 5h | -| Backend: Validaciones | 2h | -| Backend: Tests | 2h | -| Frontend: RolesListPage | 4h | -| Frontend: RoleForm | 4h | -| Frontend: Tests | 2h | -| **Total** | **19h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-002.md b/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-002.md deleted file mode 100644 index aaf1b44..0000000 --- a/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-002.md +++ /dev/null @@ -1,177 +0,0 @@ -# US-MGN003-002: Gestion de Permisos - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN003-002 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 5 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** ver el catalogo de permisos disponibles agrupados por modulo -**Para** asignar los permisos correctos a cada rol - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar permisos agrupados - -```gherkin -Given un admin autenticado con permiso "permissions:read" -When accede a GET /api/v1/permissions -Then el sistema retorna permisos agrupados por modulo - And cada modulo incluye nombre y lista de permisos - And cada permiso incluye code, name, description - And no incluye permisos deprecados -``` - -### Escenario 2: Buscar permisos - -```gherkin -Given 40 permisos en el sistema -When el admin busca "users" -Then el sistema retorna solo permisos que contienen "users" - And mantiene agrupacion por modulo - And la busqueda es case-insensitive -``` - -### Escenario 3: Mostrar permisos en selector - -```gherkin -Given admin creando un nuevo rol -When ve el selector de permisos -Then los permisos estan agrupados por modulo - And puede expandir/colapsar cada modulo - And puede seleccionar multiples permisos - And puede seleccionar "todos" de un modulo -``` - -### Escenario 4: Wildcard en permisos - -```gherkin -Given un rol con permiso "users:*" -When se visualizan los permisos del rol -Then aparece "users:*" como seleccionado - And indica que incluye todos los permisos de usuarios - And los permisos individuales aparecen como "incluidos" -``` - ---- - -## Mockup / Wireframe - -``` -Selector de Permisos (en modal de crear/editar rol) -┌────────────────────────────────────────────────────────────────┐ -│ Buscar permisos: [________________] [Seleccionar todos] │ -├────────────────────────────────────────────────────────────────┤ -│ │ -│ ▼ Gestion de Usuarios (7 permisos) [☑ Todos] │ -│ ┌──────────────────────────────────────────────────────────┐│ -│ │ ☑ users:read - Leer usuarios ││ -│ │ ☑ users:create - Crear usuarios ││ -│ │ ☐ users:update - Actualizar usuarios ││ -│ │ ☐ users:delete - Eliminar usuarios ││ -│ │ ☐ users:activate - Activar/Desactivar usuarios ││ -│ │ ☐ users:export - Exportar lista de usuarios ││ -│ │ ☐ users:import - Importar usuarios ││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ▶ Roles y Permisos (6 permisos) [☐ Todos] │ -│ │ -│ ▶ Inventario (9 permisos) [☐ Todos] │ -│ │ -│ ▶ Modulo Financiero (7 permisos) [☐ Todos] │ -│ │ -├────────────────────────────────────────────────────────────────┤ -│ Permisos seleccionados: 2 │ -└────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -GET /api/v1/permissions?search=users - -// Response -{ - "data": [ - { - "module": "users", - "moduleName": "Gestion de Usuarios", - "permissions": [ - { - "id": "uuid", - "code": "users:read", - "name": "Leer usuarios", - "description": "Ver listado y detalle de usuarios" - }, - // ... - ] - } - ] -} -``` - -### Permisos por Modulo - -| Modulo | Cantidad | Ejemplo | -|--------|----------|---------| -| auth | 2 | auth:sessions:read | -| users | 7 | users:read, users:create | -| roles | 6 | roles:read, roles:assign | -| tenants | 3 | tenants:read | -| settings | 2 | settings:update | -| audit | 2 | audit:read | -| reports | 4 | reports:export | -| financial | 7 | financial:transactions:create | -| inventory | 9 | inventory:products:read | - ---- - -## Definicion de Done - -- [ ] Endpoint GET /api/v1/permissions -- [ ] Busqueda por texto -- [ ] Agrupacion por modulo -- [ ] Frontend: PermissionSelector component -- [ ] Frontend: Expand/collapse por modulo -- [ ] Frontend: Seleccion multiple -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Endpoint | 2h | -| Backend: Agrupacion | 1h | -| Backend: Tests | 1h | -| Frontend: PermissionSelector | 5h | -| Frontend: Tests | 2h | -| **Total** | **11h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-003.md b/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-003.md deleted file mode 100644 index 6c1fdcf..0000000 --- a/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-003.md +++ /dev/null @@ -1,211 +0,0 @@ -# US-MGN003-003: Asignacion de Roles a Usuarios - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN003-003 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** poder asignar y quitar roles a los usuarios -**Para** controlar que acciones puede realizar cada usuario en el sistema - ---- - -## Criterios de Aceptacion - -### Escenario 1: Asignar rol a usuario - -```gherkin -Given un usuario "juan@empresa.com" con rol "user" - And un rol "vendedor" disponible -When el admin asigna rol "vendedor" al usuario -Then el usuario tiene roles ["user", "vendedor"] - And el usuario tiene permisos de ambos roles combinados - And se registra en auditoria quien hizo la asignacion -``` - -### Escenario 2: Quitar rol de usuario - -```gherkin -Given un usuario con roles ["admin", "manager"] -When el admin quita el rol "manager" -Then el usuario solo tiene rol ["admin"] - And pierde los permisos exclusivos de "manager" - And los cambios son inmediatos -``` - -### Escenario 3: Asignacion masiva - -```gherkin -Given 10 usuarios seleccionados - And rol "vendedor" disponible -When el admin asigna "vendedor" a todos -Then los 10 usuarios tienen rol "vendedor" agregado - And responde con resumen: { success: 10, failed: 0 } -``` - -### Escenario 4: Proteccion de super_admin - -```gherkin -Given un admin autenticado (no super_admin) -When intenta asignar rol "super_admin" a un usuario -Then el sistema responde con status 403 - And el mensaje es "Solo Super Admin puede asignar este rol" -``` - -### Escenario 5: No quitar ultimo super_admin - -```gherkin -Given solo 1 usuario con rol "super_admin" -When se intenta quitar el rol -Then el sistema responde con status 400 - And el mensaje es "Debe existir al menos un Super Admin" -``` - -### Escenario 6: Ver permisos efectivos - -```gherkin -Given usuario con roles ["admin", "vendedor"] -When consulta GET /api/v1/users/:id/permissions -Then retorna union de permisos de ambos roles - And indica cuales son directos y cuales heredados -``` - ---- - -## Mockup / Wireframe - -``` -Modal: Gestionar Roles de Usuario -┌──────────────────────────────────────────────────────────────────┐ -│ ROLES DE: Juan Perez (juan@empresa.com) │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ Selecciona los roles para este usuario: │ -│ │ -│ ┌────────────────────────────────────────────────────────────┐ │ -│ │ 🔒 Super Admin Acceso total al sistema [ ] │ │ -│ │ 🔒 Admin Gestion completa del tenant [✓] │ │ -│ │ 🔒 Manager Supervision operativa [ ] │ │ -│ │ 🔒 User Acceso basico [✓] │ │ -│ │ Vendedor Equipo de ventas [✓] │ │ -│ │ Contador Area contable [ ] │ │ -│ │ Almacenista Gestion de inventario [ ] │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ Roles seleccionados: 3 (Admin, User, Vendedor) │ -│ Permisos efectivos: 45 permisos │ -│ │ -│ [Ver detalle de permisos] │ -│ │ -│ [ Cancelar ] [ Guardar Cambios ] │ -└──────────────────────────────────────────────────────────────────┘ - -Vista: Usuarios de un Rol (desde detalle de rol) -┌──────────────────────────────────────────────────────────────────┐ -│ Rol: Vendedor [+ Agregar Usuarios]│ -├──────────────────────────────────────────────────────────────────┤ -│ Usuarios con este rol (8): │ -│ │ -│ | Avatar | Nombre | Email | Desde | ⚙ | -│ |--------|----------------|--------------------|-----------|----| -│ | 👤 | Carlos Lopez | carlos@empresa.com | 01/12/2025 | 🗑| -│ | 👤 | Maria Garcia | maria@empresa.com | 15/11/2025 | 🗑| -│ | 👤 | Pedro Martinez | pedro@empresa.com | 10/11/2025 | 🗑| -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Desde usuario - asignar roles -PUT /api/v1/users/:userId/roles -{ "roleIds": ["role-1", "role-2"] } - -// Desde rol - asignar usuarios -POST /api/v1/roles/:roleId/users -{ "userIds": ["user-1", "user-2"] } - -// Ver usuarios de un rol -GET /api/v1/roles/:roleId/users - -// Ver permisos efectivos -GET /api/v1/users/:userId/permissions -``` - -### Respuestas - -```typescript -// Asignacion exitosa -{ - "userId": "user-uuid", - "roles": ["admin", "vendedor"], - "effectivePermissions": ["users:*", "sales:*", ...] -} - -// Asignacion masiva -{ - "results": { - "success": ["user-1", "user-2"], - "failed": [ - { "userId": "user-3", "reason": "Usuario no encontrado" } - ] - } -} -``` - ---- - -## Definicion de Done - -- [ ] Endpoint PUT /api/v1/users/:id/roles -- [ ] Endpoint POST /api/v1/roles/:id/users -- [ ] Endpoint GET /api/v1/roles/:id/users -- [ ] Endpoint GET /api/v1/users/:id/permissions -- [ ] Proteccion de rol super_admin -- [ ] Validacion de ultimo super_admin -- [ ] Calculo de permisos efectivos -- [ ] Frontend: RoleAssignmentModal -- [ ] Frontend: UsersOfRole view -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Endpoints | 4h | -| Backend: Validaciones | 2h | -| Backend: Permisos efectivos | 2h | -| Backend: Tests | 2h | -| Frontend: RoleAssignmentModal | 4h | -| Frontend: BulkAssignment | 2h | -| Frontend: Tests | 2h | -| **Total** | **18h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-004.md b/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-004.md deleted file mode 100644 index 86c1cae..0000000 --- a/docs/01-fase-foundation/MGN-003-roles/historias-usuario/US-MGN003-004.md +++ /dev/null @@ -1,230 +0,0 @@ -# US-MGN003-004: Control de Acceso RBAC - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN003-004 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** desarrollador del sistema -**Quiero** que el sistema valide automaticamente los permisos en cada request -**Para** garantizar que los usuarios solo accedan a funcionalidades autorizadas - ---- - -## Criterios de Aceptacion - -### Escenario 1: Acceso con permiso valido - -```gherkin -Given un usuario con permiso "users:read" - And endpoint GET /api/v1/users requiere "users:read" -When el usuario hace la solicitud -Then el sistema permite el acceso - And retorna status 200 con los datos -``` - -### Escenario 2: Acceso denegado - -```gherkin -Given un usuario con permiso "users:read" solamente - And endpoint DELETE /api/v1/users/:id requiere "users:delete" -When el usuario intenta eliminar -Then el sistema retorna status 403 - And el mensaje es "No tienes permiso para realizar esta accion" - And NO revela que permiso falta (seguridad) -``` - -### Escenario 3: Wildcard permite acceso - -```gherkin -Given un usuario con permiso "users:*" - And endpoint requiere "users:delete" -When el usuario hace la solicitud -Then el sistema permite el acceso - And wildcard cubre el permiso especifico -``` - -### Escenario 4: Super Admin bypass - -```gherkin -Given un usuario con rol "super_admin" - And endpoint requiere "cualquier:permiso" -When el usuario hace la solicitud -Then el sistema permite el acceso - And no valida permisos especificos -``` - -### Escenario 5: Permisos alternativos (OR) - -```gherkin -Given endpoint con @AnyPermission('users:update', 'users:admin') - And usuario tiene solo "users:admin" -When el usuario hace la solicitud -Then el sistema permite el acceso - And basta con tener UNO de los permisos -``` - -### Escenario 6: Owner puede acceder - -```gherkin -Given endpoint PATCH /api/v1/users/:id con @OwnerOrPermission('users:update') - And usuario accede a su propio perfil (id = su id) - And usuario NO tiene permiso "users:update" -When el usuario actualiza su perfil -Then el sistema permite el acceso - And ser owner del recurso es suficiente -``` - -### Escenario 7: Cache de permisos - -```gherkin -Given permisos de usuario cacheados -When el admin cambia roles del usuario -Then el cache se invalida inmediatamente - And siguiente request recalcula permisos -``` - ---- - -## Mockup / Wireframe - -``` -Flujo de validacion (interno del sistema): - -┌─────────────────────────────────────────────────────────────────┐ -│ Request HTTP │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 1. JwtAuthGuard │ │ -│ │ - Valida token JWT │ │ -│ │ - Extrae usuario del token │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 2. TenantGuard │ │ -│ │ - Verifica tenant del usuario │ │ -│ │ - Aplica contexto de tenant │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 3. RbacGuard │ │ -│ │ - Lee decoradores @Permissions, @Roles │ │ -│ │ - Obtiene permisos efectivos (cache 5min) │ │ -│ │ - Valida segun modo (AND/OR) │ │ -│ │ - Super Admin bypass │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 4. Controller │ │ -│ │ - Ejecuta logica de negocio │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### Decoradores Disponibles - -```typescript -// Requiere TODOS los permisos (AND) -@Permissions('users:read', 'users:update') - -// Requiere AL MENOS UNO (OR) -@AnyPermission('users:update', 'users:admin') - -// Requiere uno de los roles -@Roles('admin', 'manager') - -// Ruta publica (sin auth) -@Public() - -// Owner o permiso -@OwnerOrPermission('users:update') -``` - -### Uso en Controllers - -```typescript -@Controller('api/v1/users') -@UseGuards(JwtAuthGuard, TenantGuard, RbacGuard) -export class UsersController { - - @Get() - @Permissions('users:read') - findAll() { } - - @Delete(':id') - @Permissions('users:delete') - remove() { } - - @Patch(':id') - @OwnerOrPermission('users:update') - update() { } -} -``` - -### Cache de Permisos - -```typescript -// TTL: 5 minutos -// Key: rbac:permissions:{userId} -// Invalidacion: al cambiar roles del usuario -``` - ---- - -## Definicion de Done - -- [ ] Decoradores @Permissions, @AnyPermission, @Roles -- [ ] Decorador @Public para rutas sin auth -- [ ] Decorador @OwnerOrPermission -- [ ] RbacGuard implementado -- [ ] OwnerGuard implementado -- [ ] Cache de permisos en Redis -- [ ] Invalidacion de cache automatica -- [ ] Log de accesos denegados -- [ ] Tests unitarios para guards -- [ ] Tests de integracion RBAC -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Decoradores | 2h | -| Backend: RbacGuard | 4h | -| Backend: OwnerGuard | 2h | -| Backend: Cache service | 2h | -| Backend: Invalidacion cache | 2h | -| Backend: Tests guards | 3h | -| Backend: Tests integracion | 3h | -| **Total** | **18h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-001.md b/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-001.md deleted file mode 100644 index e24728b..0000000 --- a/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-001.md +++ /dev/null @@ -1,184 +0,0 @@ -# US-MGN004-001: Gestion de Tenants - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN004-001 | -| **Modulo** | MGN-004 Tenants | -| **RF Relacionado** | RF-TENANT-001 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 13 | -| **Sprint** | TBD | - ---- - -## Historia de Usuario - -**Como** Platform Admin -**Quiero** gestionar los tenants de la plataforma (crear, ver, editar, suspender, eliminar) -**Para** administrar las organizaciones que usan el sistema ERP - ---- - -## Criterios de Aceptacion - -### AC-001: Listar tenants - -```gherkin -Given soy Platform Admin autenticado -When accedo a GET /api/v1/platform/tenants -Then veo lista paginada de tenants - And cada tenant muestra: nombre, slug, estado, fecha creacion - And puedo filtrar por estado (created, trial, active, suspended) - And puedo buscar por nombre o slug -``` - -### AC-002: Crear nuevo tenant - -```gherkin -Given soy Platform Admin autenticado -When envio POST /api/v1/platform/tenants con: - | name | slug | subdomain | - | Empresa XYZ | empresa-xyz | xyz | -Then se crea el tenant con estado "created" - And se crean los settings por defecto - And se genera registro de auditoria -``` - -### AC-003: Validacion de slug unico - -```gherkin -Given existe tenant con slug "empresa-abc" -When intento crear tenant con slug "empresa-abc" -Then el sistema responde con status 409 - And el mensaje indica que el slug ya existe -``` - -### AC-004: Ver detalle de tenant - -```gherkin -Given soy Platform Admin autenticado - And existe tenant "tenant-123" -When accedo a GET /api/v1/platform/tenants/tenant-123 -Then veo toda la informacion del tenant - And incluye: nombre, slug, subdominio, estado, fechas - And incluye: subscripcion actual, uso de recursos -``` - -### AC-005: Actualizar tenant - -```gherkin -Given soy Platform Admin autenticado - And existe tenant "tenant-123" con nombre "Empresa Original" -When envio PATCH /api/v1/platform/tenants/tenant-123 con: - | name | subdominio | - | Empresa Nueva | nuevo | -Then el nombre cambia a "Empresa Nueva" - And el subdominio cambia a "nuevo" - And se actualiza updated_at y updated_by -``` - -### AC-006: Suspender tenant - -```gherkin -Given soy Platform Admin autenticado - And tenant "tenant-123" esta activo -When cambio estado a "suspended" con razon "Falta de pago" -Then el estado cambia a "suspended" - And se registra suspended_at y suspension_reason - And usuarios del tenant no pueden acceder -``` - -### AC-007: Reactivar tenant suspendido - -```gherkin -Given soy Platform Admin autenticado - And tenant "tenant-123" esta suspendido -When cambio estado a "active" -Then el estado cambia a "active" - And se limpian campos de suspension - And usuarios pueden acceder nuevamente -``` - -### AC-008: Programar eliminacion - -```gherkin -Given soy Platform Admin autenticado - And tenant "tenant-123" existe -When envio DELETE /api/v1/platform/tenants/tenant-123 -Then el estado cambia a "pending_deletion" - And se programa eliminacion en 30 dias - And se notifica al owner del tenant -``` - -### AC-009: Restaurar tenant pendiente de eliminacion - -```gherkin -Given soy Platform Admin autenticado - And tenant "tenant-123" esta en "pending_deletion" -When envio POST /api/v1/platform/tenants/tenant-123/restore -Then el estado cambia a "active" - And se cancela la eliminacion programada -``` - -### AC-010: Cambiar contexto a tenant (switch) - -```gherkin -Given soy Platform Admin autenticado -When envio POST /api/v1/platform/switch-tenant/tenant-123 -Then mi contexto cambia a tenant "tenant-123" - And puedo ver datos de ese tenant - And la accion queda auditada -``` - ---- - -## Tareas Tecnicas - -| ID | Tarea | Estimacion | -|----|-------|------------| -| T-001 | Crear entidad Tenant con TypeORM | 1 SP | -| T-002 | Implementar TenantsService CRUD | 3 SP | -| T-003 | Implementar PlatformTenantsController | 2 SP | -| T-004 | Crear DTOs con validaciones | 1 SP | -| T-005 | Implementar transicion de estados | 2 SP | -| T-006 | Implementar switch tenant | 2 SP | -| T-007 | Tests unitarios | 2 SP | -| **Total** | | **13 SP** | - ---- - -## Notas de Implementacion - -- Estados validos: created -> trial -> active -> suspended -> pending_deletion -> deleted -- Solo Platform Admin puede gestionar tenants -- Switch tenant debe quedar en logs de auditoria -- Eliminacion real solo despues de 30 dias de grace period - ---- - -## Mockup de Referencia - -Ver RF-TENANT-001.md seccion Mockup. - ---- - -## Dependencias - -| Tipo | Descripcion | -|------|-------------| -| Backend | Schema core_tenants creado | -| Auth | JWT con claims de platform_admin | -| Audit | Sistema de auditoria funcionando | - ---- - -## Definition of Done - -- [ ] Endpoints implementados y documentados en Swagger -- [ ] Validaciones de DTOs completas -- [ ] Transiciones de estado implementadas -- [ ] Tests unitarios con >80% coverage -- [ ] Code review aprobado -- [ ] Logs de auditoria funcionando diff --git a/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-002.md b/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-002.md deleted file mode 100644 index 954a670..0000000 --- a/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-002.md +++ /dev/null @@ -1,178 +0,0 @@ -# US-MGN004-002: Configuracion de Tenant - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN004-002 | -| **Modulo** | MGN-004 Tenants | -| **RF Relacionado** | RF-TENANT-002 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Sprint** | TBD | - ---- - -## Historia de Usuario - -**Como** Tenant Admin -**Quiero** configurar la informacion de mi organizacion (empresa, branding, regional, seguridad) -**Para** personalizar el sistema segun las necesidades de mi empresa - ---- - -## Criterios de Aceptacion - -### AC-001: Ver configuracion actual - -```gherkin -Given soy Tenant Admin autenticado -When accedo a GET /api/v1/tenant/settings -Then veo configuracion completa organizada en secciones: - | company | branding | regional | operational | security | - And valores propios sobrescriben defaults - And se indica cuales son valores heredados -``` - -### AC-002: Actualizar informacion de empresa - -```gherkin -Given soy Tenant Admin autenticado -When envio PATCH /api/v1/tenant/settings con: - | company.companyName | company.taxId | - | Mi Empresa S.A. | MEMP850101ABC | -Then se actualizan los campos enviados - And otros campos de company no se modifican - And se actualiza updated_at y updated_by -``` - -### AC-003: Personalizar branding - -```gherkin -Given soy Tenant Admin autenticado -When actualizo branding con: - | primaryColor | secondaryColor | - | #FF5733 | #33FF57 | -Then los colores se guardan - And la UI refleja los nuevos colores -``` - -### AC-004: Subir logo - -```gherkin -Given soy Tenant Admin autenticado -When envio POST /api/v1/tenant/settings/logo con imagen PNG de 500KB -Then el logo se almacena en storage - And se genera version thumbnail - And se actualiza branding.logo con URL - And el logo aparece en la UI -``` - -### AC-005: Validar formato y tamano de logo - -```gherkin -Given soy Tenant Admin autenticado -When intento subir logo de 10MB -Then el sistema responde con status 400 - And el mensaje indica "Tamano maximo: 5MB" -``` - -### AC-006: Configurar zona horaria - -```gherkin -Given soy Tenant Admin autenticado -When actualizo regional.defaultTimezone a "America/New_York" -Then las fechas se muestran en hora de Nueva York - And los reportes usan esa zona horaria -``` - -### AC-007: Configurar politicas de seguridad - -```gherkin -Given soy Tenant Admin autenticado -When actualizo security con: - | passwordMinLength | mfaRequired | - | 12 | true | -Then nuevos usuarios deben usar password de 12+ caracteres - And se requiere MFA para todos los usuarios - And usuarios existentes deben activar MFA en siguiente login -``` - -### AC-008: Resetear a valores por defecto - -```gherkin -Given soy Tenant Admin con branding personalizado -When envio POST /api/v1/tenant/settings/reset con: - | sections | ["branding"] | -Then branding vuelve a valores por defecto de plataforma - And otras secciones no se modifican -``` - -### AC-009: Validacion de campos - -```gherkin -Given soy Tenant Admin autenticado -When envio color con formato invalido "#GGG" -Then el sistema responde con status 400 - And el mensaje indica formato invalido -``` - -### AC-010: Herencia de defaults - -```gherkin -Given tenant sin configuracion de dateFormat - And plataforma tiene default "DD/MM/YYYY" -When consulto settings -Then dateFormat muestra "DD/MM/YYYY" - And se indica que es valor heredado (_inherited) -``` - ---- - -## Tareas Tecnicas - -| ID | Tarea | Estimacion | -|----|-------|------------| -| T-001 | Crear entidad TenantSettings | 1 SP | -| T-002 | Implementar TenantSettingsService | 2 SP | -| T-003 | Implementar merge con defaults | 1 SP | -| T-004 | Crear endpoint de upload de logo | 1 SP | -| T-005 | Implementar reset a defaults | 1 SP | -| T-006 | Tests unitarios | 2 SP | -| **Total** | | **8 SP** | - ---- - -## Notas de Implementacion - -- Settings se almacenan como JSONB en PostgreSQL -- Merge inteligente: solo sobrescribir campos enviados -- Logo se almacena en storage externo (S3, GCS, etc.) -- Cambios de seguridad aplican en siguiente login - ---- - -## Mockup de Referencia - -Ver RF-TENANT-002.md seccion Mockup. - ---- - -## Dependencias - -| Tipo | Descripcion | -|------|-------------| -| Backend | Tenant existente (US-MGN004-001) | -| Storage | Servicio de storage configurado | -| Config | Defaults de plataforma definidos | - ---- - -## Definition of Done - -- [ ] CRUD de settings funcionando -- [ ] Merge con defaults implementado -- [ ] Upload de logo con validaciones -- [ ] Reset a defaults funcionando -- [ ] Validaciones de DTOs completas -- [ ] Tests unitarios con >80% coverage diff --git a/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-003.md b/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-003.md deleted file mode 100644 index aad30c0..0000000 --- a/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-003.md +++ /dev/null @@ -1,205 +0,0 @@ -# US-MGN004-003: Aislamiento de Datos Multi-Tenant - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN004-003 | -| **Modulo** | MGN-004 Tenants | -| **RF Relacionado** | RF-TENANT-003 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 13 | -| **Sprint** | TBD | - ---- - -## Historia de Usuario - -**Como** Usuario del sistema -**Quiero** que mis datos esten completamente aislados de otros tenants -**Para** garantizar la seguridad y privacidad de la informacion de mi organizacion - ---- - -## Criterios de Aceptacion - -### AC-001: Usuario solo ve datos de su tenant - -```gherkin -Given usuario de Tenant A autenticado - And 100 productos en Tenant A - And 50 productos en Tenant B -When consulta GET /api/v1/products -Then solo ve los 100 productos de Tenant A - And no ve ningun producto de Tenant B -``` - -### AC-002: No acceso a recurso de otro tenant - -```gherkin -Given usuario de Tenant A autenticado - And producto "P-001" pertenece a Tenant B -When intenta GET /api/v1/products/P-001 -Then el sistema responde con status 404 - And el mensaje es "Recurso no encontrado" - And NO revela que el recurso existe en otro tenant -``` - -### AC-003: Asignacion automatica de tenant_id - -```gherkin -Given usuario de Tenant A autenticado -When crea un producto sin especificar tenant_id -Then el sistema asigna automaticamente tenant_id de Tenant A - And el producto queda en Tenant A -``` - -### AC-004: No permitir modificar tenant_id - -```gherkin -Given usuario de Tenant A autenticado - And producto existente en Tenant A -When intenta actualizar tenant_id a Tenant B -Then el sistema responde con status 400 - And el mensaje es "No se puede cambiar el tenant de un recurso" -``` - -### AC-005: RLS previene acceso sin contexto - -```gherkin -Given conexion a base de datos sin app.current_tenant_id -When se ejecuta SELECT * FROM core_users.users -Then el resultado es vacio - And no se produce error - And RLS previene acceso a datos -``` - -### AC-006: TenantGuard valida tenant activo - -```gherkin -Given usuario con token JWT valido - And tenant en estado "suspended" -When intenta acceder a cualquier endpoint -Then el sistema responde con status 403 - And el mensaje indica "Tenant suspendido" -``` - -### AC-007: TenantGuard detecta trial expirado - -```gherkin -Given usuario de tenant en estado "trial" - And trial_ends_at fue hace 2 dias -When intenta acceder a cualquier endpoint -Then el sistema responde con status 403 - And el mensaje indica "Periodo de prueba expirado" -``` - -### AC-008: Platform Admin switch explicito - -```gherkin -Given Platform Admin autenticado - And tiene acceso a todos los tenants -When NO ha hecho switch a ningun tenant -Then no puede ver datos de ningun tenant - And debe hacer POST /api/v1/platform/switch-tenant/:id primero -``` - -### AC-009: Middleware setea contexto PostgreSQL - -```gherkin -Given request autenticado con tenant_id en JWT -When pasa por TenantContextMiddleware -Then se ejecuta SET app.current_tenant_id = :tenantId - And todas las queries posteriores filtran por ese tenant -``` - -### AC-010: Indices optimizados por tenant - -```gherkin -Given tabla users con 1M registros distribuidos en 100 tenants -When usuario de Tenant A busca por email -Then la query usa indice compuesto (tenant_id, email) - And tiempo de respuesta < 50ms -``` - ---- - -## Tareas Tecnicas - -| ID | Tarea | Estimacion | -|----|-------|------------| -| T-001 | Crear migracion RLS policies | 3 SP | -| T-002 | Implementar TenantGuard | 2 SP | -| T-003 | Implementar TenantContextMiddleware | 2 SP | -| T-004 | Crear TenantBaseEntity | 1 SP | -| T-005 | Crear TenantAwareService base | 2 SP | -| T-006 | Tests de aislamiento | 3 SP | -| **Total** | | **13 SP** | - ---- - -## Notas de Implementacion - -### Migracion RLS - -```sql --- Habilitar RLS en tablas -ALTER TABLE core_users.users ENABLE ROW LEVEL SECURITY; - --- Funcion para obtener tenant actual -CREATE OR REPLACE FUNCTION current_tenant_id() -RETURNS UUID AS $$ -BEGIN - RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID; -END; -$$ LANGUAGE plpgsql STABLE; - --- Politica de SELECT -CREATE POLICY tenant_isolation_select ON core_users.users - FOR SELECT USING (tenant_id = current_tenant_id()); - --- Politica de INSERT -CREATE POLICY tenant_isolation_insert ON core_users.users - FOR INSERT WITH CHECK (tenant_id = current_tenant_id()); -``` - -### TenantBaseEntity - -```typescript -export abstract class TenantBaseEntity { - @Column({ name: 'tenant_id' }) - tenantId: string; - - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} -``` - ---- - -## Mockup de Referencia - -Ver RF-TENANT-003.md diagramas de arquitectura. - ---- - -## Dependencias - -| Tipo | Descripcion | -|------|-------------| -| Database | PostgreSQL 12+ con RLS | -| Auth | JWT con tenant_id claim | -| Backend | Schema core_tenants creado | - ---- - -## Definition of Done - -- [ ] RLS habilitado en TODAS las tablas de negocio -- [ ] TenantGuard implementado y aplicado globalmente -- [ ] TenantContextMiddleware seteando variable de sesion -- [ ] Tests de aislamiento pasando (cross-tenant access blocked) -- [ ] Performance test con queries filtradas por tenant -- [ ] Security review aprobado -- [ ] Documentacion de patrones para nuevas tablas diff --git a/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-004.md b/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-004.md deleted file mode 100644 index b5a3933..0000000 --- a/docs/01-fase-foundation/MGN-004-tenants/historias-usuario/US-MGN004-004.md +++ /dev/null @@ -1,211 +0,0 @@ -# US-MGN004-004: Subscripciones y Limites - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN004-004 | -| **Modulo** | MGN-004 Tenants | -| **RF Relacionado** | RF-TENANT-004 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 13 | -| **Sprint** | TBD | - ---- - -## Historia de Usuario - -**Como** Tenant Admin -**Quiero** gestionar la subscripcion de mi organizacion y ver los limites de uso -**Para** controlar los costos y asegurar que tengo los recursos necesarios - ---- - -## Criterios de Aceptacion - -### AC-001: Ver subscripcion actual - -```gherkin -Given soy Tenant Admin autenticado - And tengo plan Professional activo -When accedo a GET /api/v1/tenant/subscription -Then veo: - | plan | Professional | - | price | $99 USD/mes | - | status | active | - | nextRenewal | 01/01/2026 | - And veo uso actual vs limites -``` - -### AC-002: Ver uso de recursos - -```gherkin -Given soy Tenant Admin con plan Professional (50 usuarios, 25GB) - And tengo 35 usuarios activos y 18GB usado -When consulto GET /api/v1/tenant/subscription/usage -Then veo: - | recurso | actual | limite | porcentaje | - | usuarios | 35 | 50 | 70% | - | storage | 18GB | 25GB | 72% | - | apiCalls | 5000 | 50000 | 10% | -``` - -### AC-003: Bloqueo por limite de usuarios - -```gherkin -Given tenant con plan Starter (10 usuarios) - And 10 usuarios activos (100%) -When Admin intenta crear usuario #11 -Then el sistema responde con status 402 - And el mensaje indica limite alcanzado - And sugiere planes con mayor limite -``` - -### AC-004: Bloqueo por modulo no incluido - -```gherkin -Given tenant con plan Starter (sin CRM) -When usuario intenta GET /api/v1/crm/contacts -Then el sistema responde con status 402 - And el mensaje indica modulo no disponible - And sugiere planes que incluyen CRM -``` - -### AC-005: Ver planes disponibles - -```gherkin -Given soy usuario autenticado -When accedo a GET /api/v1/subscription/plans -Then veo lista de planes publicos - And cada plan muestra: nombre, precio, limites, features - And puedo comparar con mi plan actual -``` - -### AC-006: Upgrade de plan - -```gherkin -Given tenant en Starter ($29/mes) al dia 15 del mes -When solicito upgrade a Professional ($99/mes) -Then sistema calcula prorrateo: $35 (15 dias de diferencia) - And proceso pago de $35 - And plan cambia inmediatamente a Professional - And nuevos limites aplican de inmediato -``` - -### AC-007: Cancelar subscripcion - -```gherkin -Given soy Tenant Admin con subscripcion activa -When solicito cancelar subscripcion -Then sistema muestra encuesta de salida - And confirmo cancelacion - And subscripcion se marca cancel_at_period_end = true - And acceso continua hasta fin de periodo pagado -``` - -### AC-008: Trial expira - -```gherkin -Given tenant en Trial de 14 dias -When pasan los 14 dias sin subscribirse -Then estado cambia a "trial_expired" - And usuarios no pueden acceder (excepto admin para subscribirse) - And datos se conservan -``` - -### AC-009: Verificar limite antes de accion - -```gherkin -Given endpoint que crea usuarios - And tiene decorador @CheckLimit('users') -When se ejecuta la accion -Then LimitGuard verifica limite automaticamente - And si excede, retorna 402 antes de ejecutar logica -``` - -### AC-010: Ver historial de facturas - -```gherkin -Given soy Tenant Admin autenticado -When accedo a GET /api/v1/tenant/invoices -Then veo lista de facturas: - | fecha | concepto | monto | estado | - | 01/12/2025 | Professional - Dic | $99.00 | Pagado | - And puedo descargar PDF de cada factura -``` - ---- - -## Tareas Tecnicas - -| ID | Tarea | Estimacion | -|----|-------|------------| -| T-001 | Crear entidades Plan, Subscription, Invoice | 2 SP | -| T-002 | Implementar SubscriptionsService | 3 SP | -| T-003 | Implementar LimitGuard | 2 SP | -| T-004 | Implementar ModuleGuard | 1 SP | -| T-005 | Implementar TenantUsageService | 2 SP | -| T-006 | Crear endpoints de subscripcion | 2 SP | -| T-007 | Tests unitarios | 2 SP | -| **Total** | | **14 SP** | - ---- - -## Notas de Implementacion - -### Decorador CheckLimit - -```typescript -@Post() -@CheckLimit('users') -create(@Body() dto: CreateUserDto) { } -``` - -### Decorador CheckModule - -```typescript -@Get() -@CheckModule('crm') -findAllContacts() { } -``` - -### Response 402 Payment Required - -```json -{ - "statusCode": 402, - "error": "Payment Required", - "message": "Limite de usuarios alcanzado (10/10)", - "upgradeOptions": [ - { "planId": "...", "name": "Professional", "newLimit": 50 } - ] -} -``` - ---- - -## Mockup de Referencia - -Ver RF-TENANT-004.md seccion Mockup. - ---- - -## Dependencias - -| Tipo | Descripcion | -|------|-------------| -| Backend | Tenants y Settings (US-MGN004-001, US-MGN004-002) | -| Payment | Integracion con Stripe/PayPal (futuro) | -| Scheduler | Jobs para renovacion y notificaciones | - ---- - -## Definition of Done - -- [ ] Planes y subscripciones funcionando -- [ ] LimitGuard bloqueando excesos -- [ ] ModuleGuard verificando acceso a modulos -- [ ] Upgrade/downgrade funcionando -- [ ] Calculo de prorrateo correcto -- [ ] Historial de facturas visible -- [ ] Tests unitarios con >80% coverage diff --git a/docs/02-definicion-modulos/INDICE-MODULOS.md b/docs/02-definicion-modulos/INDICE-MODULOS.md index 8b62ddb..5f89ea0 100644 --- a/docs/02-definicion-modulos/INDICE-MODULOS.md +++ b/docs/02-definicion-modulos/INDICE-MODULOS.md @@ -4,14 +4,15 @@ | Metrica | Valor | |---------|-------| -| Total Modulos | 19 | +| Total Modulos | 22 | | Modulos P0 (Criticos) | 4 | | Modulos P1 (Core) | 6 | | Modulos P2 (Extended) | 5 | -| Modulos P3 (SaaS) | 4 | +| Modulos P3 (SaaS Platform) | 4 | +| Modulos P3 (IA Intelligence) | 3 | | Completados | 0 | | En Desarrollo | 2 | -| Planificados | 17 | +| Planificados | 20 | --- @@ -52,9 +53,17 @@ | Codigo | Modulo | Estado | Progreso | Docs | |--------|--------|--------|----------|------| | MGN-016 | [billing](#mgn-016-billing) | Planificado | 0% | [Ver](./MGN-016-billing/) | -| MGN-017 | [payments-pos](#mgn-017-payments-pos) | Planificado | 0% | [Ver](./MGN-017-payments-pos/) | -| MGN-018 | [whatsapp-business](#mgn-018-whatsapp-business) | Planificado | 0% | [Ver](./MGN-018-whatsapp-business/) | -| MGN-019 | [ai-agents](#mgn-019-ai-agents) | Planificado | 0% | [Ver](./MGN-019-ai-agents/) | +| MGN-017 | [plans](#mgn-017-plans) | Planificado | 0% | [Ver](./MGN-017-plans/) | +| MGN-018 | [webhooks](#mgn-018-webhooks) | Planificado | 0% | [Ver](./MGN-018-webhooks/) | +| MGN-019 | [feature-flags](#mgn-019-feature-flags) | Planificado | 0% | [Ver](./MGN-019-feature-flags/) | + +### P3 - IA Intelligence + +| Codigo | Modulo | Estado | Progreso | Docs | +|--------|--------|--------|----------|------| +| MGN-020 | [ai-integration](#mgn-020-ai-integration) | Planificado | 0% | [Ver](./MGN-020-ai-integration/) | +| MGN-021 | [whatsapp-business](#mgn-021-whatsapp-business) | Planificado | 0% | [Ver](./MGN-021-whatsapp-business/) | +| MGN-022 | [mcp-server](#mgn-022-mcp-server) | Planificado | 0% | [Ver](./MGN-022-mcp-server/) | --- @@ -376,98 +385,204 @@ ### MGN-016: Billing -**Proposito:** Facturacion SaaS por asientos (per-seat billing) +**Proposito:** Suscripciones y pagos SaaS con Stripe | Aspecto | Detalle | |---------|---------| | Schema BD | `billing` | -| Tablas | tenant_owners, payment_methods, invoices, invoice_lines, payments, coupons, coupon_redemptions, usage_records, subscription_history | +| Tablas | subscriptions, invoices, payments, payment_methods | | Endpoints | 15+ | -| Dependencias | MGN-001, MGN-004 Tenants | -| Usado por | MGN-017, MGN-018, MGN-019 | +| Dependencias | MGN-001 Auth, MGN-004 Tenants | +| Usado por | MGN-017 Plans | +| Integracion | Stripe | **Funcionalidades:** -- Suscripciones mensuales por tenant -- Modelo per-seat: base + extra seats × precio -- Feature flags por plan (Starter, Growth, Enterprise) -- Gestion de propietarios de tenant -- Cupones y descuentos -- Historial de cambios de suscripcion -- Facturacion automatica -- Metodos de pago (tarjetas, SPEI) +- Suscripciones recurrentes (mensual/anual) +- Trial gratuito configurable +- Upgrade/downgrade con prorateo +- Webhooks Stripe sincronizados +- Portal de cliente Stripe integrado +- Facturas y recibos automaticos +- Multiples monedas (USD, MXN) --- -### MGN-017: Payments POS +### MGN-017: Plans -**Proposito:** Integraciones con terminales de pago (MercadoPago, Clip) +**Proposito:** Planes, limites y feature gating | Aspecto | Detalle | |---------|---------| -| Schema BD | `integrations` | -| Tablas | payment_providers, payment_credentials, payment_terminals, payment_transactions, refunds, webhook_logs, reconciliation_batches | -| Endpoints | 20+ | -| Dependencias | MGN-001, MGN-004, MGN-010 Financial | -| Usado por | Verticales POS | +| Schema BD | `plans` | +| Tablas | plans, plan_features, tenant_limits | +| Endpoints | 10+ | +| Dependencias | MGN-016 Billing | +| Usado por | Todos los modulos | **Funcionalidades:** -- Multi-provider: MercadoPago (OAuth) y Clip (API Keys) -- Registro de terminales por ubicacion -- Procesamiento de transacciones -- Reembolsos parciales y totales -- Webhooks para notificaciones -- Conciliacion de transacciones -- Generacion de asientos contables -- Manejo de comisiones por provider +- Planes Free, Starter, Pro, Enterprise +- Feature gating por plan +- Limites numericos (usuarios, storage, etc.) +- Verificacion de features en tiempo real +- Upgrade paths configurables + +**Planes propuestos:** + +| Plan | Precio | Usuarios | Storage | Features | +|------|--------|----------|---------|----------| +| Free | $0/mes | 1 | 100MB | Core basico | +| Starter | $29/mes | 5 | 1GB | + API access | +| Pro | $79/mes | 20 | 10GB | + AI + Webhooks | +| Enterprise | $199/mes | Unlimited | Unlimited | + Custom + SLA | --- -### MGN-018: WhatsApp Business +### MGN-018: Webhooks -**Proposito:** Integracion con WhatsApp Business Cloud API +**Proposito:** Webhooks outbound con firma HMAC | Aspecto | Detalle | |---------|---------| -| Schema BD | `messaging` | -| Tablas | whatsapp_accounts, whatsapp_templates, whatsapp_conversations, whatsapp_messages, chatbot_flows, chatbot_nodes, chatbot_sessions, whatsapp_campaigns | -| Endpoints | 25+ | -| Dependencias | MGN-001, MGN-004, MGN-005 Catalogs | -| Usado por | MGN-019 AI Agents | +| Schema BD | `webhooks` | +| Tablas | webhook_endpoints, webhook_events, webhook_deliveries | +| Endpoints | 8+ | +| Dependencias | MGN-001 Auth, MGN-004 Tenants | +| Usado por | Integraciones externas | +| Cola | BullMQ (Redis) | **Funcionalidades:** -- Cuentas de WhatsApp Business por tenant -- Templates de mensajes (HSM) aprobados por Meta -- Conversaciones bidireccionales -- Chatbots con flujos visuales -- Campañas de marketing masivo -- Opt-in/opt-out de contactos -- Metricas de entrega y lectura -- Webhooks de WhatsApp Cloud API +- Registro de endpoints por tenant +- Firma HMAC-SHA256 en cada request +- Politica de reintentos exponencial +- Log de entregas y respuestas +- Eventos: user.*, subscription.*, invoice.* + +**Eventos disponibles:** + +| Evento | Descripcion | +|--------|-------------| +| user.created | Usuario creado | +| subscription.created | Suscripcion creada | +| subscription.cancelled | Suscripcion cancelada | +| invoice.paid | Factura pagada | --- -### MGN-019: AI Agents +### MGN-019: Feature Flags -**Proposito:** Agentes inteligentes con RAG para automatizacion +**Proposito:** Feature flags por tenant y usuario | Aspecto | Detalle | |---------|---------| -| Schema BD | `ai_agents` | -| Tablas | agents, knowledge_bases, agent_knowledge_bases, kb_documents, kb_chunks, tool_definitions, agent_tools, conversations, messages, tool_executions, feedback, usage_logs | -| Endpoints | 30+ | -| Dependencias | MGN-001, MGN-004, MGN-018 WhatsApp | +| Schema BD | `feature_flags` | +| Tablas | flags, tenant_flags, user_flags | +| Endpoints | 6+ | +| Dependencias | MGN-001 Auth, MGN-004 Tenants | +| Usado por | Todos los modulos | + +**Funcionalidades:** +- Flags globales con valor default +- Override por tenant +- Override por usuario +- Rollout gradual (porcentaje) +- A/B testing +- Evaluacion por contexto + +--- + +### MGN-020: AI Integration + +**Proposito:** Gateway LLM multi-proveedor (OpenRouter) + +| Aspecto | Detalle | +|---------|---------| +| Schema BD | `ai` | +| Tablas | ai_conversations, ai_messages, ai_usage_logs | +| Endpoints | 10+ | +| Dependencias | MGN-001 Auth, MGN-004 Tenants | +| Usado por | MGN-021 WhatsApp Business | +| Integracion | OpenRouter | + +**Funcionalidades:** +- Acceso a 50+ modelos LLM +- Cambio de modelo sin codigo +- Fallback automatico entre modelos +- Token tracking por tenant +- Rate limiting por plan +- Configuracion por tenant + +**Modelos soportados:** + +| Modelo | Costo/1M tokens | Uso | +|--------|-----------------:|-----| +| Claude 3 Haiku | $0.25 | Default | +| Claude 3 Sonnet | $3.00 | Premium | +| GPT-4o-mini | $0.15 | Fallback | +| Mistral 7B | $0.06 | Economico | + +--- + +### MGN-021: WhatsApp Business + +**Proposito:** WhatsApp Business con IA conversacional + +| Aspecto | Detalle | +|---------|---------| +| Schema BD | `whatsapp` | +| Tablas | whatsapp_sessions, whatsapp_messages, whatsapp_templates | +| Endpoints | 15+ | +| Dependencias | MGN-020 AI Integration | | Usado por | Verticales | +| Integracion | Meta WhatsApp Cloud API | **Funcionalidades:** -- Agentes configurables por tenant -- Knowledge bases con documentos -- RAG con pgvector (embeddings 1536 dims) -- Definicion de tools/funciones -- Conversaciones con historial -- Ejecucion de tools en tiempo real -- Feedback de usuarios (thumbs up/down) -- Metricas de uso y tokens -- Integracion con WhatsApp como canal +- Webhook receiver Meta Cloud API +- Procesamiento de mensajes (texto, audio, imagen) +- Transcripcion de audio (Whisper) +- OCR de imagenes (Google Vision) +- IA conversacional con contexto +- Templates HSM pre-aprobados +- Envio de mensajes outbound + +**Flujo de mensaje:** +``` +Cliente -> WhatsApp -> Webhook -> LLM -> MCP -> Respuesta +``` + +--- + +### MGN-022: MCP Server + +**Proposito:** Model Context Protocol Server para herramientas de negocio + +| Aspecto | Detalle | +|---------|---------| +| Schema BD | N/A (usa otros schemas) | +| Endpoints | N/A (Protocolo MCP) | +| Dependencias | MGN-020 AI Integration | +| Usado por | Clientes MCP (Claude Desktop, IA conversacional) | +| Protocolo | MCP (Anthropic) | +| Puerto | 3142 | + +**Funcionalidades:** +- Servidor MCP standalone +- Herramientas de negocio expuestas a LLMs +- Contexto multi-tenant +- Autenticacion por API key + +**Herramientas disponibles:** + +| Tool | Descripcion | +|------|-------------| +| product_list | Listar productos | +| product_details | Detalles de producto | +| product_availability | Disponibilidad | +| inventory_stock | Consultar stock | +| inventory_low_stock | Alertas stock bajo | +| sales_create_order | Crear orden | +| sales_order_status | Estado de orden | +| customer_search | Buscar cliente | +| fiado_balance | Balance fiados | --- @@ -482,15 +597,13 @@ Fase 1: Foundation Fase 2: Core Business ├── MGN-005 Catalogs -├── Partners (parte de Catalogs) -├── Products (parte de Catalogs) +├── MGN-006 Settings +├── MGN-010 Financial ├── MGN-011 Inventory ├── MGN-012 Purchasing -├── MGN-013 Sales -└── MGN-010 Financial +└── MGN-013 Sales Fase 3: Extended -├── MGN-006 Settings ├── MGN-007 Audit ├── MGN-008 Notifications ├── MGN-009 Reports @@ -499,9 +612,14 @@ Fase 3: Extended Fase 4: SaaS Platform ├── MGN-016 Billing (depende de MGN-004) -├── MGN-017 Payments POS (depende de MGN-010) -├── MGN-018 WhatsApp Business (depende de MGN-005) -└── MGN-019 AI Agents (depende de MGN-018) +├── MGN-017 Plans (depende de MGN-016) +├── MGN-018 Webhooks (depende de MGN-004) +└── MGN-019 Feature Flags (depende de MGN-004) + +Fase 5: IA Intelligence +├── MGN-020 AI Integration (depende de MGN-004) +├── MGN-021 WhatsApp Business (depende de MGN-020) +└── MGN-022 MCP Server (depende de MGN-020) ``` --- @@ -510,50 +628,86 @@ Fase 4: SaaS Platform ```mermaid graph TD - MGN001[MGN-001 Auth] --> MGN002[MGN-002 Users] - MGN001 --> MGN004[MGN-004 Tenants] - MGN002 --> MGN003[MGN-003 Roles] - MGN004 --> MGN005[MGN-005 Catalogs] - MGN005 --> MGN010[MGN-010 Financial] - MGN005 --> MGN011[MGN-011 Inventory] - MGN011 --> MGN012[MGN-012 Purchasing] - MGN011 --> MGN013[MGN-013 Sales] - MGN010 --> MGN012 - MGN010 --> MGN013 - MGN005 --> MGN014[MGN-014 CRM] - MGN002 --> MGN015[MGN-015 Projects] + subgraph "Fase 1: Foundation" + MGN001[MGN-001 Auth] + MGN002[MGN-002 Users] + MGN003[MGN-003 Roles] + MGN004[MGN-004 Tenants] + end - %% SaaS Platform Modules - MGN004 --> MGN016[MGN-016 Billing] - MGN010 --> MGN017[MGN-017 Payments POS] - MGN005 --> MGN018[MGN-018 WhatsApp] - MGN018 --> MGN019[MGN-019 AI Agents] + subgraph "Fase 2-3: Core/Extended" + MGN005[MGN-005 Catalogs] + MGN010[MGN-010 Financial] + MGN011[MGN-011 Inventory] + MGN008[MGN-008 Notifications] + end + + subgraph "Fase 4: SaaS Platform" + MGN016[MGN-016 Billing] + MGN017[MGN-017 Plans] + MGN018[MGN-018 Webhooks] + MGN019[MGN-019 Feature Flags] + end + + subgraph "Fase 5: IA Intelligence" + MGN020[MGN-020 AI Integration] + MGN021[MGN-021 WhatsApp Business] + MGN022[MGN-022 MCP Server] + end + + MGN001 --> MGN002 + MGN001 --> MGN004 + MGN002 --> MGN003 + MGN004 --> MGN005 + MGN005 --> MGN010 + MGN005 --> MGN011 + + %% SaaS Platform + MGN004 --> MGN016 MGN016 --> MGN017 - MGN016 --> MGN018 + MGN004 --> MGN018 + MGN004 --> MGN019 + + %% IA Intelligence + MGN004 --> MGN020 + MGN020 --> MGN021 + MGN020 --> MGN022 + MGN021 --> MGN022 ``` --- ## Proximas Acciones -1. **Completar documentacion MGN-001 Auth** - - DDL Specification - - Especificacion Backend - - User Stories +1. **Completar documentacion Fase 1: Foundation** + - MGN-001 Auth: DDL, Backend Spec, User Stories + - MGN-002 Users: DDL, Backend Spec, User Stories + - MGN-003 Roles: RBAC design + - MGN-004 Tenants: RLS implementation -2. **Completar documentacion MGN-002 Users** - - Mismos entregables +2. **Planificar Fase 4: SaaS Platform** + - MGN-016 Billing: Stripe integration, webhooks + - MGN-017 Plans: Feature gating, limits + - MGN-018 Webhooks: HMAC signing, retries + - MGN-019 Feature Flags: Rollout, A/B testing -3. **Iniciar MGN-003 Roles** - - Requerimientos funcionales - - Diseno de RBAC - -4. **Planificar Fase 4: SaaS Platform** - - MGN-016 Billing: Per-seat pricing, feature flags - - MGN-017 Payments POS: MercadoPago, Clip - - MGN-018 WhatsApp Business: Cloud API, chatbots - - MGN-019 AI Agents: RAG, pgvector, tools +3. **Planificar Fase 5: IA Intelligence** + - MGN-020 AI Integration: OpenRouter gateway + - MGN-021 WhatsApp Business: Meta Cloud API + - MGN-022 MCP Server: Business tools --- -*Ultima actualizacion: Diciembre 2025* +## Referencias + +| Documento | Path | +|-----------|------| +| Vision General | [VISION-ERP-CORE.md](../00-vision-general/VISION-ERP-CORE.md) | +| Arquitectura SaaS | [ARQUITECTURA-SAAS.md](../00-vision-general/ARQUITECTURA-SAAS.md) | +| Arquitectura IA | [ARQUITECTURA-IA.md](../00-vision-general/ARQUITECTURA-IA.md) | +| Integraciones | [INTEGRACIONES-EXTERNAS.md](../00-vision-general/INTEGRACIONES-EXTERNAS.md) | +| Stack Tecnologico | [STACK-TECNOLOGICO.md](../00-vision-general/STACK-TECNOLOGICO.md) | + +--- + +*Ultima actualizacion: Enero 2026* diff --git a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-001.md b/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-001.md deleted file mode 100644 index 5b11ab8..0000000 --- a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-001.md +++ /dev/null @@ -1,249 +0,0 @@ -# US-MGN005-001: Gestionar Contactos - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN005-001 | -| **Modulo** | MGN-005 Catalogs | -| **Sprint** | Sprint 3 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-CATALOG-001 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** gestionar contactos (clientes, proveedores, empleados) -**Para** mantener un directorio centralizado de todas las entidades con las que interactua mi organizacion - ---- - -## Descripcion - -El sistema debe permitir crear, editar, listar y eliminar contactos. Cada contacto puede ser clasificado como cliente, proveedor, empleado u otro tipo. Los contactos son la base para operaciones comerciales, facturacion, pagos y CRM. - -### Contexto - -- Base para modulos de ventas, compras y CRM -- Soporta multiple tipos de contacto -- Incluye datos fiscales y de contacto -- Vinculable a usuarios del sistema - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar contactos - -```gherkin -Given un usuario autenticado con permiso "catalogs.contacts.read" -When accede a GET /api/v1/catalogs/contacts -Then el sistema responde con status 200 - And retorna lista paginada de contactos del tenant - And cada contacto incluye: id, name, contactType, email, phone, isActive - And los resultados estan ordenados por nombre por defecto -``` - -### Escenario 2: Buscar contactos - -```gherkin -Given un usuario con permiso "catalogs.contacts.read" -When envia GET /api/v1/catalogs/contacts?search=acme -Then retorna contactos cuyo nombre, email o tax_id contengan "acme" - And la busqueda es case-insensitive - -When envia GET /api/v1/catalogs/contacts?type=customer -Then retorna solo contactos de tipo "customer" - -When envia GET /api/v1/catalogs/contacts?tags=vip,prioritario -Then retorna contactos que tengan cualquiera de esos tags -``` - -### Escenario 3: Crear contacto - -```gherkin -Given un usuario con permiso "catalogs.contacts.create" -When envia POST /api/v1/catalogs/contacts con datos validos: - | name | "ACME Corporation" | - | contactType | "customer" | - | email | "info@acme.com" | - | taxId | "RFC123456789" | -Then el sistema responde con status 201 - And retorna el contacto creado con id generado - And el contacto pertenece al tenant actual -``` - -### Escenario 4: Crear contacto con datos incompletos - -```gherkin -Given un usuario con permiso "catalogs.contacts.create" -When envia POST sin el campo "name" -Then el sistema responde con status 400 - And el mensaje indica "name es requerido" - -When envia POST con email invalido -Then el sistema responde con status 400 - And el mensaje indica "email no es valido" - -When envia POST con contactType no valido -Then el sistema responde con status 400 - And el mensaje indica "contactType debe ser: customer, supplier, employee, other" -``` - -### Escenario 5: Actualizar contacto - -```gherkin -Given un contacto existente con id "uuid-123" - And un usuario con permiso "catalogs.contacts.update" -When envia PATCH /api/v1/catalogs/contacts/uuid-123 - | phone | "+52 555 123 4567" | -Then el sistema responde con status 200 - And retorna el contacto actualizado - And updated_at se actualiza -``` - -### Escenario 6: Eliminar contacto (soft delete) - -```gherkin -Given un contacto existente sin referencias en otros modulos - And un usuario con permiso "catalogs.contacts.delete" -When envia DELETE /api/v1/catalogs/contacts/uuid-123 -Then el sistema responde con status 204 - And el contacto se marca como is_active = false - And el contacto ya no aparece en listados por defecto -``` - -### Escenario 7: Contacto con referencias - -```gherkin -Given un contacto que tiene facturas asociadas -When se intenta eliminar el contacto -Then el sistema responde con status 409 - And el mensaje indica "Contacto tiene registros asociados" - And sugiere desactivar en lugar de eliminar -``` - -### Escenario 8: Ver detalle con direcciones - -```gherkin -Given un contacto con multiples direcciones -When envia GET /api/v1/catalogs/contacts/uuid-123?include=addresses -Then retorna el contacto con array de direcciones - And cada direccion incluye: type, street, city, state, country, postalCode, isDefault -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| CONTACTOS [+ Nuevo Contacto] | -+------------------------------------------------------------------+ -| [Buscar...] [Tipo: Todos v] [Tags: v] [Solo activos ✓] | -+------------------------------------------------------------------+ -| □ | NOMBRE | TIPO | EMAIL | TELEFONO | -+------------------------------------------------------------------+ -| □ | ACME Corp | Cliente | info@acme.com | 555-1234 | -| □ | Proveedor XYZ | Proveedor | ventas@xyz.com | 555-5678 | -| □ | Juan Perez | Empleado | juan@empresa.com| 555-9012 | -+------------------------------------------------------------------+ -| < 1 2 3 ... 10 > Mostrando 1-20 de 156 | -+------------------------------------------------------------------+ - -Modal: Nuevo/Editar Contacto -+------------------------------------------------------------------+ -| NUEVO CONTACTO [X] | -+------------------------------------------------------------------+ -| Tipo de Contacto* | -| ( ) Cliente ( ) Proveedor ( ) Empleado ( ) Otro | -| | -| Nombre/Razon Social* | RFC/NIF | -| [________________________] | [________________] | -| | -| Email | Telefono | -| [________________________] | [________________] | -| | -| [+ Agregar Direccion] | -| | -| Tags: [VIP] [x] [Prioritario] [x] [+ Agregar] | -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /catalogs/contacts | Listar con filtros | -| GET | /catalogs/contacts/:id | Obtener detalle | -| POST | /catalogs/contacts | Crear contacto | -| PATCH | /catalogs/contacts/:id | Actualizar | -| DELETE | /catalogs/contacts/:id | Eliminar (soft) | -| POST | /catalogs/contacts/:id/addresses | Agregar direccion | -| DELETE | /catalogs/contacts/:id/addresses/:aid | Eliminar direccion | - -### Validaciones - -| Campo | Regla | Mensaje Error | -|-------|-------|---------------| -| name | Required, MaxLength(255) | "Nombre es requerido" | -| contactType | Required, Enum | "Tipo de contacto invalido" | -| email | Optional, IsEmail | "Email no es valido" | -| taxId | Optional, MaxLength(50) | - | -| phone | Optional, Pattern | "Formato de telefono invalido" | - ---- - -## Definicion de Done - -- [ ] CRUD completo de contactos implementado -- [ ] Busqueda por nombre, email, taxId -- [ ] Filtros por tipo y tags -- [ ] Gestion de direcciones -- [ ] Soft delete implementado -- [ ] Validacion de referencias antes de eliminar -- [ ] RLS aplicado (tenant isolation) -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] UI de listado y formulario -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| MGN-001 Auth | Autenticacion JWT | -| MGN-003 RBAC | Permisos catalogs.contacts.* | -| MGN-004 Tenants | RLS por tenant | -| core_catalogs schema | Tablas contacts, addresses | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Facturacion | Requiere clientes/proveedores | -| CxC/CxP | Requiere contactos | -| CRM | Requiere contactos | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-002.md b/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-002.md deleted file mode 100644 index 37a2b44..0000000 --- a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-002.md +++ /dev/null @@ -1,193 +0,0 @@ -# US-MGN005-002: Consultar Paises y Estados - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN005-002 | -| **Modulo** | MGN-005 Catalogs | -| **Sprint** | Sprint 3 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 3 | -| **Estado** | Ready | -| **RF Relacionado** | RF-CATALOG-002 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** consultar catalogos de paises y estados/provincias -**Para** seleccionar ubicaciones geograficas al capturar direcciones - ---- - -## Descripcion - -El sistema debe proporcionar catalogos globales de paises (ISO 3166-1) y estados/provincias por pais. Estos catalogos son de solo lectura para usuarios y se usan en formularios de direccion. - -### Contexto - -- Catalogos globales compartidos entre tenants -- Basados en estandares ISO -- Usados en direcciones de contactos -- Incluyen agrupaciones regionales - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar paises - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/catalogs/countries -Then el sistema responde con status 200 - And retorna lista de paises ordenados por nombre - And cada pais incluye: code (ISO), name, dialCode, currencyCode - And los paises inactivos no se incluyen -``` - -### Escenario 2: Buscar paises - -```gherkin -Given un usuario autenticado -When envia GET /api/v1/catalogs/countries?search=mex -Then retorna paises cuyo nombre o codigo contengan "mex" - And Mexico aparece en los resultados - -When envia GET /api/v1/catalogs/countries?region=latam -Then retorna solo paises del grupo "latam" -``` - -### Escenario 3: Obtener estados de un pais - -```gherkin -Given un usuario autenticado -When envia GET /api/v1/catalogs/countries/MX/states -Then el sistema responde con status 200 - And retorna lista de estados de Mexico - And cada estado incluye: code, name, countryCode - And ordenados alfabeticamente por nombre -``` - -### Escenario 4: Pais sin estados - -```gherkin -Given un pais pequeno sin subdivision administrativa -When envia GET /api/v1/catalogs/countries/MC/states -Then el sistema responde con status 200 - And retorna array vacio -``` - -### Escenario 5: Pais no encontrado - -```gherkin -Given un codigo de pais que no existe "XX" -When envia GET /api/v1/catalogs/countries/XX -Then el sistema responde con status 404 - And el mensaje indica "Pais no encontrado" -``` - -### Escenario 6: Listar grupos regionales - -```gherkin -Given un usuario autenticado -When envia GET /api/v1/catalogs/country-groups -Then retorna grupos como: latam, europe, nafta, asia - And cada grupo incluye: code, name, countries[] -``` - ---- - -## Mockup / Wireframe - -``` -Selector de Pais (Dropdown con busqueda) -+----------------------------------+ -| Pais * | -| [🔍 Buscar pais... v] | -+----------------------------------+ -| 🇲🇽 Mexico | -| 🇺🇸 Estados Unidos | -| 🇪🇸 Espana | -| 🇦🇷 Argentina | -| ... | -+----------------------------------+ - -Selector de Estado (Dependiente del pais) -+----------------------------------+ -| Estado * | -| [Selecciona estado... v] | -+----------------------------------+ -| Aguascalientes | -| Baja California | -| Baja California Sur | -| Campeche | -| ... | -+----------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /catalogs/countries | Listar paises | -| GET | /catalogs/countries/:code | Detalle de pais | -| GET | /catalogs/countries/:code/states | Estados del pais | -| GET | /catalogs/country-groups | Listar grupos | - -### Cache - -- Catalogos se cachean en Redis por 24 horas -- Cache key: `catalogs:countries`, `catalogs:countries:MX:states` -- Invalidacion manual por admin - -### Datos ISO - -- Paises: ISO 3166-1 alpha-2 -- Estados: ISO 3166-2 - ---- - -## Definicion de Done - -- [ ] Endpoints de consulta implementados -- [ ] Datos seed de paises y estados cargados -- [ ] Cache Redis configurado -- [ ] Componente CountrySelect implementado -- [ ] Componente StateSelect (dependiente) implementado -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla countries | Con datos ISO 3166-1 | -| Tabla states | Con datos ISO 3166-2 | -| Redis | Para cache | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN005-001 | Direcciones de contactos | -| Cualquier formulario | Con seleccion de ubicacion | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-003.md b/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-003.md deleted file mode 100644 index 58e0b0d..0000000 --- a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-003.md +++ /dev/null @@ -1,247 +0,0 @@ -# US-MGN005-003: Gestionar Monedas y Tipos de Cambio - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN005-003 | -| **Modulo** | MGN-005 Catalogs | -| **Sprint** | Sprint 3 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-CATALOG-003 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador financiero -**Quiero** gestionar las monedas habilitadas y sus tipos de cambio -**Para** realizar operaciones multi-moneda con conversiones correctas - ---- - -## Descripcion - -El sistema debe permitir habilitar monedas para el tenant, definir una moneda base, y registrar tipos de cambio historicos. Las conversiones se realizan usando el tipo de cambio vigente a una fecha dada. - -### Contexto - -- Soporte multi-moneda por tenant -- Una moneda base obligatoria -- Tipos de cambio con fecha efectiva -- Conversion automatica en transacciones - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar monedas disponibles - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/catalogs/currencies -Then el sistema responde con status 200 - And retorna catalogo global de monedas ISO 4217 - And cada moneda incluye: code, name, symbol, decimalPlaces -``` - -### Escenario 2: Ver monedas habilitadas del tenant - -```gherkin -Given un usuario con permiso "catalogs.currencies.read" -When accede a GET /api/v1/catalogs/tenant-currencies -Then retorna solo monedas habilitadas para el tenant - And indica cual es la moneda base (isBase: true) - And muestra el tipo de cambio actual vs moneda base -``` - -### Escenario 3: Habilitar moneda para tenant - -```gherkin -Given un usuario con permiso "catalogs.currencies.manage" -When envia POST /api/v1/catalogs/tenant-currencies - | currencyCode | "EUR" | - | isBase | false | -Then el sistema responde con status 201 - And la moneda EUR queda habilitada para el tenant -``` - -### Escenario 4: Definir moneda base - -```gherkin -Given un tenant sin moneda base definida - And un usuario con permiso "catalogs.currencies.manage" -When envia POST /api/v1/catalogs/tenant-currencies - | currencyCode | "MXN" | - | isBase | true | -Then MXN se establece como moneda base - And todas las conversiones usaran MXN como referencia -``` - -### Escenario 5: Cambiar moneda base (no permitido) - -```gherkin -Given un tenant con moneda base MXN - And existen transacciones registradas -When intenta cambiar la moneda base a USD -Then el sistema responde con status 400 - And el mensaje indica "No se puede cambiar moneda base con transacciones existentes" -``` - -### Escenario 6: Registrar tipo de cambio - -```gherkin -Given un usuario con permiso "catalogs.currencies.manage" -When envia POST /api/v1/catalogs/exchange-rates - | fromCurrency | "USD" | - | toCurrency | "MXN" | - | rate | 17.25 | - | effectiveDate | "2025-12-05" | -Then el sistema responde con status 201 - And el tipo de cambio queda registrado -``` - -### Escenario 7: Consultar tipo de cambio historico - -```gherkin -Given tipos de cambio registrados para USD/MXN: - | date | rate | - | 2025-12-01 | 17.00 | - | 2025-12-05 | 17.25 | -When consulta tipo de cambio para fecha 2025-12-03 -Then retorna rate 17.00 (el vigente a esa fecha) -``` - -### Escenario 8: Convertir monto - -```gherkin -Given tipo de cambio USD/MXN = 17.25 vigente -When envia POST /api/v1/catalogs/currencies/convert - | amount | 100 | - | from | "USD" | - | to | "MXN" | -Then retorna: - | originalAmount | 100 | - | convertedAmount | 1725 | - | rate | 17.25 | -``` - -### Escenario 9: Conversion sin tipo de cambio - -```gherkin -Given no existe tipo de cambio para EUR/MXN -When intenta convertir de EUR a MXN -Then el sistema responde con status 404 - And el mensaje indica "Tipo de cambio no encontrado" -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| MONEDAS Y TIPOS DE CAMBIO | -+------------------------------------------------------------------+ - -MONEDAS HABILITADAS -+------------------------------------------------------------------+ -| CODIGO | NOMBRE | SIMBOLO | BASE | TC ACTUAL | -+------------------------------------------------------------------+ -| MXN | Peso Mexicano | $ | ✓ | 1.0000 | -| USD | Dolar USA | $ | | 17.2500 | -| EUR | Euro | € | | 18.5000 | -+------------------------------------------------------------------+ - [+ Habilitar Moneda] - -TIPOS DE CAMBIO -+------------------------------------------------------------------+ -| [Moneda: USD v] [Fecha desde: ___] [Fecha hasta: ___] [Buscar] | -+------------------------------------------------------------------+ -| FECHA | DE | A | TASA | FUENTE | -+------------------------------------------------------------------+ -| 2025-12-05 | USD | MXN | 17.2500 | Manual | -| 2025-12-04 | USD | MXN | 17.2000 | Manual | -| 2025-12-03 | USD | MXN | 17.1500 | API Banco de Mexico | -+------------------------------------------------------------------+ - [+ Nuevo Tipo de Cambio] - -CONVERTIDOR -+----------------------------------+ -| Monto: [1000 ] | -| De: [USD v] A: [MXN v] | -| Fecha: [2025-12-05] | -| [=== Convertir ===] | -| | -| Resultado: $17,250.00 MXN | -| Tasa: 1 USD = 17.25 MXN | -+----------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /catalogs/currencies | Catalogo global | -| GET | /catalogs/tenant-currencies | Monedas del tenant | -| POST | /catalogs/tenant-currencies | Habilitar moneda | -| DELETE | /catalogs/tenant-currencies/:code | Deshabilitar | -| GET | /catalogs/exchange-rates | Listar tipos de cambio | -| POST | /catalogs/exchange-rates | Registrar tipo | -| GET | /catalogs/exchange-rates/current | Tipo actual | -| POST | /catalogs/currencies/convert | Convertir monto | - -### Precision - -- Tipos de cambio: DECIMAL(18,8) -- Montos: DECIMAL(18,4) -- Redondeo: HALF_UP segun decimales de moneda - ---- - -## Definicion de Done - -- [ ] CRUD monedas del tenant -- [ ] Registro de tipos de cambio -- [ ] Funcion de conversion -- [ ] Busqueda de tipo por fecha -- [ ] Validacion moneda base -- [ ] Componente CurrencySelect -- [ ] Componente CurrencyInput -- [ ] Tests unitarios -- [ ] Tests e2e -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla currencies | Catalogo ISO 4217 | -| Tabla tenant_currencies | Monedas habilitadas | -| Tabla exchange_rates | Tipos de cambio | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| MGN-010 Financial | Operaciones multi-moneda | -| Facturacion | Facturas en moneda extranjera | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-004.md b/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-004.md deleted file mode 100644 index b9714fd..0000000 --- a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-004.md +++ /dev/null @@ -1,222 +0,0 @@ -# US-MGN005-004: Gestionar Unidades de Medida - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN005-004 | -| **Modulo** | MGN-005 Catalogs | -| **Sprint** | Sprint 3 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 5 | -| **Estado** | Ready | -| **RF Relacionado** | RF-CATALOG-004 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** gestionar unidades de medida y realizar conversiones entre ellas -**Para** manejar productos e inventarios con diferentes unidades - ---- - -## Descripcion - -El sistema debe permitir definir unidades de medida organizadas por categoria (peso, volumen, longitud, etc.) y establecer factores de conversion dentro de cada categoria. Cada tenant puede personalizar sus unidades. - -### Contexto - -- Categorias de unidades predefinidas -- Unidades base por categoria -- Factores de conversion configurables -- Usadas en productos e inventario - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar categorias de unidades - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/catalogs/uom/categories -Then el sistema responde con status 200 - And retorna categorias: weight, volume, length, area, time, unit - And cada categoria indica su unidad base -``` - -### Escenario 2: Listar unidades de una categoria - -```gherkin -Given un usuario con permiso "catalogs.uom.read" -When accede a GET /api/v1/catalogs/uom?category=weight -Then retorna unidades de peso del tenant - And cada unidad incluye: code, name, category, conversionFactor, isBase - And kg (kilogramo) es la unidad base (factor 1) -``` - -### Escenario 3: Crear unidad de medida - -```gherkin -Given un usuario con permiso "catalogs.uom.manage" -When envia POST /api/v1/catalogs/uom - | code | "lb" | - | name | "Libra" | - | category | "weight" | - | conversionFactor | 0.453592 | -Then el sistema responde con status 201 - And la unidad queda registrada - And 1 lb = 0.453592 kg -``` - -### Escenario 4: Crear unidad base - -```gherkin -Given una categoria sin unidad base definida -When envia POST con isBase: true -Then la unidad se crea con conversionFactor = 1 - And se marca como unidad base de la categoria -``` - -### Escenario 5: Convertir unidades - -```gherkin -Given unidades definidas: - | kg | factor 1.0 (base) | - | lb | factor 0.453592 | - | g | factor 0.001 | -When envia POST /api/v1/catalogs/uom/convert - | quantity | 10 | - | fromUnit | "lb" | - | toUnit | "kg" | -Then retorna: - | originalQuantity | 10 | - | convertedQuantity | 4.53592 | - | fromUnit | "lb" | - | toUnit | "kg" | -``` - -### Escenario 6: Conversion entre categorias diferentes - -```gherkin -Given unidades de diferentes categorias -When intenta convertir de "kg" (weight) a "m" (length) -Then el sistema responde con status 400 - And el mensaje indica "No se pueden convertir unidades de diferentes categorias" -``` - -### Escenario 7: Eliminar unidad en uso - -```gherkin -Given una unidad asignada a productos -When intenta eliminar la unidad -Then el sistema responde con status 409 - And el mensaje indica "Unidad en uso por N productos" -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| UNIDADES DE MEDIDA [+ Nueva Unidad] | -+------------------------------------------------------------------+ -| [Categoria: Todas v] [Buscar...] | -+------------------------------------------------------------------+ - -PESO (Base: Kilogramo) -+------------------------------------------------------------------+ -| CODIGO | NOMBRE | FACTOR | BASE | ACCIONES | -+------------------------------------------------------------------+ -| kg | Kilogramo | 1.0 | ✓ | [Editar] | -| g | Gramo | 0.001 | | [Editar] [Eliminar]| -| lb | Libra | 0.453592 | | [Editar] [Eliminar]| -| oz | Onza | 0.0283495 | | [Editar] [Eliminar]| -+------------------------------------------------------------------+ - -VOLUMEN (Base: Litro) -+------------------------------------------------------------------+ -| l | Litro | 1.0 | ✓ | [Editar] | -| ml | Mililitro | 0.001 | | [Editar] [Eliminar]| -| gal | Galon | 3.78541 | | [Editar] [Eliminar]| -+------------------------------------------------------------------+ - -CONVERTIDOR -+----------------------------------+ -| Cantidad: [100 ] | -| De: [lb v] | -| A: [kg v] | -| [=== Convertir ===] | -| | -| Resultado: 45.3592 kg | -+----------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /catalogs/uom/categories | Listar categorias | -| GET | /catalogs/uom | Listar unidades | -| GET | /catalogs/uom/:code | Detalle unidad | -| POST | /catalogs/uom | Crear unidad | -| PATCH | /catalogs/uom/:code | Actualizar | -| DELETE | /catalogs/uom/:code | Eliminar | -| POST | /catalogs/uom/convert | Convertir | - -### Formula de Conversion - -``` -convertedQty = originalQty * (fromUnit.factor / toUnit.factor) - -Ejemplo: 10 lb a kg -= 10 * (0.453592 / 1.0) = 4.53592 kg -``` - ---- - -## Definicion de Done - -- [ ] CRUD unidades de medida -- [ ] Categorias predefinidas con seed -- [ ] Funcion de conversion -- [ ] Validacion misma categoria -- [ ] Validacion referencias antes de eliminar -- [ ] Componente UomSelect -- [ ] Componente UomConverter -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla uom_categories | Categorias | -| Tabla uom | Unidades | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Productos | Unidad de medida | -| Inventario | Conversiones | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-005.md b/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-005.md deleted file mode 100644 index ca63592..0000000 --- a/docs/02-fase-core-business/MGN-005-catalogs/historias-usuario/US-MGN005-005.md +++ /dev/null @@ -1,243 +0,0 @@ -# US-MGN005-005: Gestionar Categorias Jerarquicas - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN005-005 | -| **Modulo** | MGN-005 Catalogs | -| **Sprint** | Sprint 3 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 5 | -| **Estado** | Ready | -| **RF Relacionado** | RF-CATALOG-005 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** crear y gestionar categorias jerarquicas -**Para** organizar productos, servicios y otros elementos en estructuras de arbol - ---- - -## Descripcion - -El sistema debe soportar categorias con multiples niveles de profundidad (jerarquia padre-hijo). Cada categoria puede tener subcategorias, formando un arbol. Se usan para clasificar productos, documentos, y otros elementos. - -### Contexto - -- Estructura de arbol multinivel -- Tipos de categoria: products, services, documents, expenses -- Navegacion breadcrumb -- Path materializado para consultas eficientes - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar categorias como arbol - -```gherkin -Given un usuario con permiso "catalogs.categories.read" -When accede a GET /api/v1/catalogs/categories?type=products -Then el sistema responde con status 200 - And retorna estructura de arbol con categorias raiz - And cada categoria incluye children[] con sus hijos -``` - -### Escenario 2: Listar categorias planas - -```gherkin -Given un usuario con permiso "catalogs.categories.read" -When accede a GET /api/v1/catalogs/categories?type=products&flat=true -Then retorna lista plana de todas las categorias - And cada categoria incluye path completo (ej: "Electrónica > Computadoras > Laptops") -``` - -### Escenario 3: Crear categoria raiz - -```gherkin -Given un usuario con permiso "catalogs.categories.manage" -When envia POST /api/v1/catalogs/categories - | code | "electronics" | - | name | "Electrónica" | - | type | "products" | - | parentId | null | -Then el sistema responde con status 201 - And la categoria se crea en nivel 1 - And path = "electronics" -``` - -### Escenario 4: Crear subcategoria - -```gherkin -Given categoria padre "Electrónica" con id "uuid-parent" -When envia POST /api/v1/catalogs/categories - | code | "computers" | - | name | "Computadoras" | - | type | "products" | - | parentId | "uuid-parent" | -Then la categoria se crea en nivel 2 - And path = "electronics.computers" -``` - -### Escenario 5: Mover categoria - -```gherkin -Given categoria "Laptops" hija de "Computadoras" -When envia PATCH /api/v1/catalogs/categories/laptops - | parentId | "uuid-tablets" | -Then "Laptops" se mueve bajo "Tablets" - And se actualizan los paths de Laptops y sus descendientes -``` - -### Escenario 6: Eliminar categoria con hijos - -```gherkin -Given categoria "Electrónica" con subcategorias -When intenta eliminar "Electrónica" -Then el sistema responde con status 400 - And el mensaje indica "Categoria tiene subcategorias" - And sugiere eliminar hijos primero o mover categoria -``` - -### Escenario 7: Obtener breadcrumb - -```gherkin -Given categoria "Laptops" con path "electronics.computers.laptops" -When accede a GET /api/v1/catalogs/categories/laptops?include=breadcrumb -Then retorna la categoria con array breadcrumb: - | [0] | Electrónica | - | [1] | Computadoras | - | [2] | Laptops | -``` - -### Escenario 8: Buscar en categorias - -```gherkin -Given categorias existentes -When envia GET /api/v1/catalogs/categories?search=laptop -Then retorna categorias cuyo nombre contenga "laptop" - And incluye el path completo para contexto -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| CATEGORIAS [+ Nueva Categoria] | -+------------------------------------------------------------------+ -| [Tipo: Productos v] [Buscar...] | -+------------------------------------------------------------------+ - -Vista Arbol: -+------------------------------------------------------------------+ -| [-] Electrónica | -| [-] Computadoras | -| [+] Laptops (3 productos) | -| [+] Desktops (5 productos) | -| [+] Telefonos | -| [+] Accesorios | -| [-] Hogar | -| [+] Muebles | -| [+] Decoracion | -| [+] Ropa | -+------------------------------------------------------------------+ - -Modal: Nueva Categoria -+------------------------------------------------------------------+ -| NUEVA CATEGORIA [X] | -+------------------------------------------------------------------+ -| Tipo* | -| [Productos v] | -| | -| Categoria Padre | -| [Seleccionar... v] | -| > Electrónica | -| > Computadoras | -| > Telefonos | -| > Hogar | -| | -| Codigo* | Nombre* | -| [laptops ] | [Laptops ] | -| | -| Descripcion | -| [________________________________] | -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /catalogs/categories | Listar (arbol o plano) | -| GET | /catalogs/categories/:id | Detalle con breadcrumb | -| POST | /catalogs/categories | Crear | -| PATCH | /catalogs/categories/:id | Actualizar/mover | -| DELETE | /catalogs/categories/:id | Eliminar | - -### Estructura LTREE - -```sql --- Path materializado para consultas eficientes -path LTREE - --- Obtener todos los descendientes -SELECT * FROM categories WHERE path <@ 'electronics'; - --- Obtener ancestros -SELECT * FROM categories WHERE path @> 'electronics.computers.laptops'; -``` - ---- - -## Definicion de Done - -- [ ] CRUD categorias con jerarquia -- [ ] Path materializado (LTREE) -- [ ] Vista arbol y plana -- [ ] Mover categoria (reparent) -- [ ] Validacion eliminar con hijos -- [ ] Breadcrumb -- [ ] Componente CategoryTree -- [ ] Componente CategorySelect (con arbol) -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Extension LTREE | PostgreSQL | -| Tabla categories | Con path y parent_id | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Productos | Clasificacion | -| Documentos | Organizacion | -| Gastos | Clasificacion contable | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-001.md b/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-001.md deleted file mode 100644 index 137c2c5..0000000 --- a/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-001.md +++ /dev/null @@ -1,234 +0,0 @@ -# US-MGN006-001: Gestionar Configuraciones del Sistema - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN006-001 | -| **Modulo** | MGN-006 Settings | -| **Sprint** | Sprint 4 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 5 | -| **Estado** | Ready | -| **RF Relacionado** | RF-SETTINGS-001 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** super administrador del sistema -**Quiero** gestionar las configuraciones globales del sistema -**Para** ajustar parametros que afectan a todos los tenants - ---- - -## Descripcion - -Las configuraciones del sistema son parametros globales que afectan el comportamiento de la plataforma. Solo pueden ser modificadas por super administradores y algunas son visibles para administradores de tenant. - -### Contexto - -- Configuraciones a nivel de plataforma -- Afectan a todos los tenants -- Algunas son publicas, otras privadas -- Incluyen validaciones por tipo de dato - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar configuraciones por categoria - -```gherkin -Given un super administrador autenticado -When accede a GET /api/v1/settings/system -Then el sistema responde con status 200 - And retorna configuraciones agrupadas por categoria - And cada setting incluye: key, value, dataType, category, isEditable - -When accede con query ?category=security -Then retorna solo configuraciones de seguridad -``` - -### Escenario 2: Ver configuraciones publicas - -```gherkin -Given un administrador de tenant (no super admin) -When accede a GET /api/v1/settings/system?public=true -Then retorna solo configuraciones donde isPublic = true - And no retorna configuraciones sensibles -``` - -### Escenario 3: Obtener configuracion especifica - -```gherkin -Given configuracion "security.max_login_attempts" existe -When accede a GET /api/v1/settings/system/security.max_login_attempts -Then retorna: - | key | "security.max_login_attempts" | - | value | 5 | - | dataType | "number" | - | description | "Intentos maximos de login" | - | isEditable | true | -``` - -### Escenario 4: Actualizar configuracion - -```gherkin -Given un super administrador - And configuracion "security.max_login_attempts" con isEditable = true -When envia PUT /api/v1/settings/system/security.max_login_attempts - | value | 3 | -Then el sistema responde con status 200 - And el valor se actualiza a 3 - And se invalida el cache de esta configuracion -``` - -### Escenario 5: Actualizar configuracion no editable - -```gherkin -Given configuracion "system.version" con isEditable = false -When intenta actualizar el valor -Then el sistema responde con status 403 - And el mensaje indica "Configuracion no es editable" -``` - -### Escenario 6: Validacion de tipo de dato - -```gherkin -Given configuracion "security.max_login_attempts" de tipo "number" -When envia valor "abc" (no numerico) -Then el sistema responde con status 400 - And el mensaje indica "El valor debe ser un numero" - -Given configuracion con validationRules: { min: 1, max: 10 } -When envia valor 15 -Then el sistema responde con status 400 - And el mensaje indica "El valor debe estar entre 1 y 10" -``` - -### Escenario 7: Resetear a valor por defecto - -```gherkin -Given configuracion modificada con valor diferente al default -When envia POST /api/v1/settings/system/security.max_login_attempts/reset -Then el valor se restaura al defaultValue - And se invalida el cache -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| CONFIGURACION DEL SISTEMA | -+------------------------------------------------------------------+ -| [Categoria: Todas v] [Solo editables ✓] [Buscar...] | -+------------------------------------------------------------------+ - -SEGURIDAD -+------------------------------------------------------------------+ -| Clave | Valor | Tipo | Acciones | -+------------------------------------------------------------------+ -| security.max_login_attempts | 5 | number | [Editar] [↺] | -| security.lockout_minutes | 30 | number | [Editar] [↺] | -| security.token_expiry_hours | 24 | number | [Editar] [↺] | -| security.password_min_length | 8 | number | [Editar] [↺] | -+------------------------------------------------------------------+ - -EMAIL -+------------------------------------------------------------------+ -| email.smtp_host | smtp... | string | [Editar] [↺] | -| email.smtp_port | 587 | number | [Editar] [↺] | -+------------------------------------------------------------------+ - -ALMACENAMIENTO -+------------------------------------------------------------------+ -| storage.max_file_size_mb | 10 | number | [Editar] [↺] | -| storage.allowed_extensions | [pdf,..] | array | [Editar] [↺] | -+------------------------------------------------------------------+ - -Modal: Editar Configuracion -+------------------------------------------------------------------+ -| EDITAR CONFIGURACION [X] | -+------------------------------------------------------------------+ -| Clave: security.max_login_attempts | -| Descripcion: Intentos maximos de login antes de bloqueo | -| | -| Tipo: number | -| Valor actual: 5 | -| Valor por defecto: 5 | -| | -| Nuevo Valor* | -| [3 ] | -| Minimo: 1 | Maximo: 10 | -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /settings/system | Listar todas | -| GET | /settings/system/:key | Obtener una | -| PUT | /settings/system/:key | Actualizar | -| POST | /settings/system/:key/reset | Resetear | - -### Tipos de Dato Soportados - -| Tipo | Ejemplo | Validaciones | -|------|---------|--------------| -| string | "smtp.example.com" | minLength, maxLength, pattern | -| number | 5 | min, max | -| boolean | true | - | -| json | {...} | jsonSchema | -| array | ["pdf","jpg"] | minItems, maxItems | -| secret | "***" | Se enmascara en lectura | - ---- - -## Definicion de Done - -- [ ] CRUD configuraciones del sistema -- [ ] Validacion por tipo de dato -- [ ] Filtro por categoria -- [ ] Filtro publicas/privadas -- [ ] Reset a valor por defecto -- [ ] Invalidacion de cache -- [ ] UI de administracion -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla system_settings | Con seed inicial | -| Cache Redis | Para performance | -| Super admin role | Permiso especial | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN006-002 | Settings de tenant heredan de sistema | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-002.md b/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-002.md deleted file mode 100644 index 95d6b0d..0000000 --- a/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-002.md +++ /dev/null @@ -1,223 +0,0 @@ -# US-MGN006-002: Gestionar Configuraciones del Tenant - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN006-002 | -| **Modulo** | MGN-006 Settings | -| **Sprint** | Sprint 4 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-SETTINGS-002 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador de tenant -**Quiero** personalizar las configuraciones de mi organizacion -**Para** adaptar el sistema a las necesidades especificas de mi empresa - ---- - -## Descripcion - -Los tenants pueden sobrescribir configuraciones heredadas del sistema o del plan de suscripcion. El sistema aplica una jerarquia: system < plan < tenant, donde el tenant tiene la ultima palabra en configuraciones permitidas. - -### Contexto - -- Herencia de configuraciones -- Override por tenant -- Limites segun plan de suscripcion -- Configuraciones especificas de negocio - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver configuraciones efectivas - -```gherkin -Given un administrador de tenant -When accede a GET /api/v1/settings/tenant -Then retorna configuraciones efectivas del tenant - And cada setting indica su origen: "system", "plan", o "custom" - And muestra valor actual y si esta sobrescrito -``` - -### Escenario 2: Herencia de configuraciones - -```gherkin -Given configuracion "branding.logo_url" no definida en tenant - And configuracion definida en plan con valor "default-logo.png" -When consulta el valor efectivo -Then retorna "default-logo.png" - And inheritedFrom = "plan" -``` - -### Escenario 3: Sobrescribir configuracion - -```gherkin -Given configuracion "branding.logo_url" heredada del plan - And usuario con permiso "settings.tenant.update" -When envia PUT /api/v1/settings/tenant/branding.logo_url - | value | "my-company-logo.png" | -Then la configuracion se guarda para el tenant - And inheritedFrom = "custom" - And isOverridden = true -``` - -### Escenario 4: Eliminar override (volver a herencia) - -```gherkin -Given configuracion "branding.logo_url" sobrescrita por tenant -When envia DELETE /api/v1/settings/tenant/branding.logo_url -Then se elimina el override - And el valor vuelve al heredado del plan -``` - -### Escenario 5: Limite por plan de suscripcion - -```gherkin -Given tenant con plan "Basic" - And configuracion "limits.max_users" tiene limite en plan = 10 -When intenta establecer valor 50 -Then el sistema responde con status 400 - And el mensaje indica "El plan Basic permite maximo 10 usuarios" -``` - -### Escenario 6: Configuracion no permitida en plan - -```gherkin -Given tenant con plan "Basic" - And configuracion "feature.advanced_reports" solo disponible en plan "Premium" -When intenta habilitar esa configuracion -Then el sistema responde con status 403 - And el mensaje indica "Caracteristica no disponible en tu plan" -``` - -### Escenario 7: Consultar por categoria - -```gherkin -Given un administrador de tenant -When accede a GET /api/v1/settings/tenant?category=branding -Then retorna solo configuraciones de branding - And incluye: logo_url, primary_color, company_name, etc. -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| CONFIGURACION DE LA EMPRESA | -+------------------------------------------------------------------+ - -BRANDING -+------------------------------------------------------------------+ -| Configuracion | Valor | Origen | Acciones | -+------------------------------------------------------------------+ -| company_name | Mi Empresa SA | Custom | [Editar] | -| logo_url | logo.png | Plan | [Override] | -| primary_color | #3498db | Custom | [Editar][↺]| -| secondary_color | #2ecc71 | System | [Override] | -+------------------------------------------------------------------+ - -LIMITES (Plan: Professional) -+------------------------------------------------------------------+ -| max_users | 50 / 50 | Plan | [🔒] | -| max_storage_gb | 80 / 100 | Plan | [🔒] | -| max_api_calls_day | 8000 / 10000 | Plan | [🔒] | -+------------------------------------------------------------------+ -[Actualizar plan para aumentar limites] - -NOTIFICACIONES -+------------------------------------------------------------------+ -| email_notifications | Habilitado | Custom | [Editar] | -| digest_frequency | daily | Custom | [Editar] | -+------------------------------------------------------------------+ - -Leyenda: -[🔒] = Controlado por plan -[↺] = Restaurar valor heredado -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /settings/tenant | Listar efectivas | -| GET | /settings/tenant/effective | Con herencia | -| PUT | /settings/tenant/:key | Sobrescribir | -| DELETE | /settings/tenant/:key | Eliminar override | - -### Logica de Herencia - -```typescript -function getEffectiveValue(key: string, tenantId: string): any { - // 1. Buscar en tenant_settings (override) - const tenantValue = await getTenantSetting(tenantId, key); - if (tenantValue && tenantValue.isOverridden) { - return { value: tenantValue.value, source: 'custom' }; - } - - // 2. Buscar en plan_settings - const planId = await getTenantPlan(tenantId); - const planValue = await getPlanSetting(planId, key); - if (planValue) { - return { value: planValue.value, source: 'plan' }; - } - - // 3. Fallback a system_settings - const systemValue = await getSystemSetting(key); - return { value: systemValue?.value || null, source: 'system' }; -} -``` - ---- - -## Definicion de Done - -- [ ] CRUD configuraciones de tenant -- [ ] Logica de herencia system < plan < tenant -- [ ] Validacion de limites por plan -- [ ] Eliminar override (volver a herencia) -- [ ] Indicador de origen en UI -- [ ] Cache con invalidacion -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN006-001 | Configuraciones del sistema | -| Tabla plan_settings | Configuraciones por plan | -| Tabla tenant_settings | Overrides | -| MGN-004 Tenants | Suscripcion y plan | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN006-003 | Preferencias de usuario | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-003.md b/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-003.md deleted file mode 100644 index 8e1db7b..0000000 --- a/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-003.md +++ /dev/null @@ -1,228 +0,0 @@ -# US-MGN006-003: Gestionar Preferencias de Usuario - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN006-003 | -| **Modulo** | MGN-006 Settings | -| **Sprint** | Sprint 4 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 5 | -| **Estado** | Ready | -| **RF Relacionado** | RF-SETTINGS-003 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** personalizar mis preferencias personales -**Para** adaptar la experiencia del sistema a mis gustos y necesidades - ---- - -## Descripcion - -Cada usuario puede configurar preferencias personales que afectan solo su experiencia. Estas preferencias incluyen idioma, zona horaria, tema visual, notificaciones y opciones de UI. Se sincronizan entre dispositivos. - -### Contexto - -- Preferencias a nivel de usuario individual -- Se aplican sobre configuraciones de tenant -- Sincronizacion entre dispositivos -- Persistencia en base de datos - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver mis preferencias - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/settings/user -Then el sistema responde con status 200 - And retorna objeto con preferencias agrupadas: - | locale | { language, timezone, dateFormat } | - | theme | { mode, primaryColor } | - | notifications | { email, push, inApp } | - | ui | { sidebarCollapsed, tablePageSize } | -``` - -### Escenario 2: Actualizar preferencias - -```gherkin -Given un usuario autenticado -When envia PATCH /api/v1/settings/user - | locale.language | "es-MX" | - | locale.timezone | "America/Mexico_City" | -Then el sistema responde con status 200 - And las preferencias se actualizan - And syncedAt se actualiza a la fecha actual -``` - -### Escenario 3: Cambiar tema oscuro/claro - -```gherkin -Given usuario con theme.mode = "light" -When envia PATCH con theme.mode = "dark" -Then la preferencia se guarda - And el frontend aplica el tema oscuro -``` - -### Escenario 4: Resetear preferencias a defaults - -```gherkin -Given usuario con preferencias personalizadas -When envia POST /api/v1/settings/user/reset -Then todas las preferencias se eliminan - And el usuario usa los valores por defecto del tenant -``` - -### Escenario 5: Resetear categoria especifica - -```gherkin -Given usuario con preferencias personalizadas -When envia POST /api/v1/settings/user/reset - | keys | ["theme.mode", "theme.primaryColor"] | -Then solo esas preferencias se resetean - And las demas permanecen -``` - -### Escenario 6: Obtener preferencias efectivas - -```gherkin -Given usuario sin preferencia de idioma definida - And tenant con idioma "es-ES" -When accede a GET /api/v1/settings/user/effective -Then retorna: - | locale.language | "es-ES" | - | source | "tenant" | -``` - -### Escenario 7: Sincronizacion entre dispositivos - -```gherkin -Given usuario cambia preferencia en navegador A -When abre sesion en navegador B - And accede a sus preferencias -Then obtiene las preferencias actualizadas - And syncedAt indica cuando se sincronizo -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| MIS PREFERENCIAS | -+------------------------------------------------------------------+ - -REGIONAL -+------------------------------------------------------------------+ -| Idioma | [Español (Mexico) v] | -| Zona Horaria | [America/Mexico_City v] | -| Formato de Fecha | [DD/MM/YYYY v] | -| Formato de Hora | [24 horas v] | -| Primer dia de semana | [Lunes v] | -+------------------------------------------------------------------+ - -APARIENCIA -+------------------------------------------------------------------+ -| Tema | ( ) Claro (•) Oscuro ( ) Sistema | -| Barra lateral | [Expandida v] | -| Densidad | ( ) Compacta (•) Normal ( ) Comoda | -+------------------------------------------------------------------+ - -NOTIFICACIONES -+------------------------------------------------------------------+ -| □ Recibir emails de notificaciones | -| □ Recibir notificaciones push | -| ✓ Mostrar notificaciones in-app | -| Frecuencia de resumen | [Diario v] | -+------------------------------------------------------------------+ - -TABLAS Y LISTAS -+------------------------------------------------------------------+ -| Elementos por pagina | [25 v] | -| Mostrar acciones | [Al pasar mouse v] | -+------------------------------------------------------------------+ - - [Restaurar Defaults] [=== Guardar ===] - -Ultima sincronizacion: hace 5 minutos -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /settings/user | Mis preferencias | -| GET | /settings/user/effective | Con herencia | -| PATCH | /settings/user | Actualizar parcial | -| POST | /settings/user/reset | Resetear | - -### Preferencias Disponibles - -| Categoria | Clave | Tipo | Default | -|-----------|-------|------|---------| -| locale | language | string | "en-US" | -| locale | timezone | string | "UTC" | -| locale | dateFormat | string | "YYYY-MM-DD" | -| locale | timeFormat | string | "24h" | -| theme | mode | enum | "system" | -| theme | primaryColor | string | null | -| ui | sidebarCollapsed | boolean | false | -| ui | tablePageSize | number | 20 | -| notifications | email | boolean | true | -| notifications | push | boolean | true | -| notifications | inApp | boolean | true | -| notifications | digestFrequency | enum | "daily" | - ---- - -## Definicion de Done - -- [ ] CRUD preferencias de usuario -- [ ] Merge con defaults de tenant -- [ ] Reset parcial y total -- [ ] Campo syncedAt actualizado -- [ ] UI de preferencias completa -- [ ] Componente ThemeToggle -- [ ] Persistencia en localStorage + DB -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN006-002 | Configuraciones de tenant (defaults) | -| Tabla user_preferences | Storage | -| Zustand store | Estado local | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| UI global | Tema, idioma, formato | -| Notificaciones | Preferencias de canales | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-004.md b/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-004.md deleted file mode 100644 index d13dc3d..0000000 --- a/docs/02-fase-core-business/MGN-006-settings/historias-usuario/US-MGN006-004.md +++ /dev/null @@ -1,276 +0,0 @@ -# US-MGN006-004: Gestionar Feature Flags - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN006-004 | -| **Modulo** | MGN-006 Settings | -| **Sprint** | Sprint 4 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-SETTINGS-004 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** controlar la disponibilidad de funcionalidades mediante feature flags -**Para** realizar despliegues graduales y controlar acceso a features - ---- - -## Descripcion - -Los feature flags permiten habilitar/deshabilitar funcionalidades sin despliegues. Soportan diferentes tipos: booleano, porcentaje (rollout gradual), y variantes (A/B testing). Pueden tener overrides por tenant o usuario. - -### Contexto - -- Control de funcionalidades en runtime -- Despliegues graduales (canary) -- Testing A/B -- Kill switches para emergencias - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar feature flags - -```gherkin -Given un super administrador -When accede a GET /api/v1/admin/features -Then el sistema responde con status 200 - And retorna lista de flags con: - | key, name, flagType, defaultValue, isActive, expiresAt | -``` - -### Escenario 2: Evaluar flag booleano - -```gherkin -Given flag "feature.dark_mode" tipo boolean con defaultValue = true -When endpoint GET /api/v1/features/feature.dark_mode es llamado -Then retorna: - | enabled | true | - | source | "default" | -``` - -### Escenario 3: Evaluar flag con override de tenant - -```gherkin -Given flag "feature.new_dashboard" defaultValue = false - And override para tenant actual con value = true -When se evalua el flag -Then retorna: - | enabled | true | - | source | "tenant_override" | -``` - -### Escenario 4: Evaluar flag con override de usuario - -```gherkin -Given flag "feature.beta_editor" con varios overrides - And override para usuario actual = true -When se evalua el flag -Then retorna: - | enabled | true | - | source | "user_override" | - And el override de usuario tiene precedencia sobre tenant -``` - -### Escenario 5: Flag tipo porcentaje - -```gherkin -Given flag "feature.new_checkout" tipo percentage - And rolloutConfig.percentage = 20 -When 100 usuarios diferentes evaluan el flag -Then aproximadamente 20 usuarios obtienen enabled = true - And la evaluacion es deterministica por userId -``` - -### Escenario 6: Flag tipo variante - -```gherkin -Given flag "experiment.button_color" tipo variant - And variantes: ["blue", "green", "red"] con pesos iguales -When usuario evalua el flag -Then retorna: - | enabled | true | - | variant | "green" | - And el usuario siempre obtiene la misma variante -``` - -### Escenario 7: Flag expirado - -```gherkin -Given flag "feature.holiday_sale" con expiresAt = ayer -When se evalua el flag -Then retorna: - | enabled | false | - | source | "expired" | -``` - -### Escenario 8: Crear override - -```gherkin -Given flag "feature.advanced_reports" - And super admin con permiso -When envia POST /api/v1/admin/features/feature.advanced_reports/overrides - | level | "tenant" | - | levelId | "tenant-uuid" | - | value | true | -Then el override se crea - And el tenant obtiene la feature habilitada -``` - -### Escenario 9: Evaluar multiples flags - -```gherkin -Given varios flags activos -When envia POST /api/v1/features/evaluate - | keys | ["feature.a", "feature.b", "feature.c"] | -Then retorna objeto con evaluacion de cada flag - And es mas eficiente que evaluar uno por uno -``` - -### Escenario 10: Uso de @RequireFeature - -```gherkin -Given endpoint protegido con @RequireFeature("feature.beta_api") - And flag deshabilitado para usuario actual -When usuario accede al endpoint -Then el sistema responde con status 403 - And el mensaje indica "Feature not available" -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| FEATURE FLAGS [+ Nuevo Flag] | -+------------------------------------------------------------------+ -| [Estado: Todos v] [Tipo: Todos v] [Buscar...] | -+------------------------------------------------------------------+ - -| FLAG | NOMBRE | TIPO | ESTADO | VALOR | -+------------------------------------------------------------------+ -| feature.dark_mode | Modo Oscuro | boolean | ✓ Act | true | -| feature.new_dash | Nuevo Dashboard | boolean | ✓ Act | false | -| feature.checkout | Nuevo Checkout | percent | ✓ Act | 20% | -| exp.button_color | Experimento Boton | variant | ✓ Act | A/B/C | -| feature.sale | Venta Especial | boolean | ✗ Exp | - | -+------------------------------------------------------------------+ - -Detalle de Flag: -+------------------------------------------------------------------+ -| feature.new_dashboard [X] | -+------------------------------------------------------------------+ -| Nombre: Nuevo Dashboard | -| Tipo: boolean | -| Valor por defecto: false | -| Expira: 2025-12-31 | -| | -| OVERRIDES | -+------------------------------------------------------------------+ -| NIVEL | ID | VALOR | ACTIVO | ACCIONES | -+------------------------------------------------------------------+ -| Tenant | Empresa ABC | true | ✓ | [Editar][Elim] | -| Tenant | Empresa XYZ | true | ✓ | [Editar][Elim] | -| User | admin@abc.com | false | ✓ | [Editar][Elim] | -+------------------------------------------------------------------+ - [+ Agregar Override] - -| ESTADISTICAS | -+------------------------------------------------------------------+ -| Evaluaciones hoy: 1,234 | -| Habilitado para: 45% de usuarios | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /features/:key | Evaluar flag | -| POST | /features/evaluate | Evaluar multiples | -| GET | /admin/features | Listar todos | -| POST | /admin/features | Crear flag | -| PATCH | /admin/features/:key | Actualizar | -| POST | /admin/features/:key/overrides | Crear override | -| DELETE | /admin/features/:key/overrides/:id | Eliminar override | - -### Algoritmo de Evaluacion - -```typescript -async function evaluate(key: string, context: Context): Promise { - const flag = await getFlag(key); - - if (!flag || !flag.isActive) return { enabled: false, source: 'not_found' }; - if (flag.expiresAt && flag.expiresAt < new Date()) return { enabled: false, source: 'expired' }; - - // Check user override - if (context.userId) { - const userOverride = await getOverride(flag.id, 'user', context.userId); - if (userOverride) return { enabled: userOverride.value, source: 'user_override' }; - } - - // Check tenant override - if (context.tenantId) { - const tenantOverride = await getOverride(flag.id, 'tenant', context.tenantId); - if (tenantOverride) return { enabled: tenantOverride.value, source: 'tenant_override' }; - } - - // Evaluate based on flag type - return evaluateByType(flag, context); -} -``` - ---- - -## Definicion de Done - -- [ ] CRUD feature flags -- [ ] Evaluacion por tipo (boolean, percentage, variant) -- [ ] Overrides por tenant y usuario -- [ ] Cache con invalidacion -- [ ] Guard @RequireFeature -- [ ] Evaluacion multiple -- [ ] UI de administracion -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla feature_flags | Definiciones | -| Tabla feature_flag_overrides | Overrides | -| Cache Redis | Para evaluacion rapida | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Cualquier feature | Puede ser controlada | -| Experimentos A/B | Variantes | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-001.md b/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-001.md deleted file mode 100644 index 8b6897d..0000000 --- a/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-001.md +++ /dev/null @@ -1,253 +0,0 @@ -# US-MGN007-001: Gestionar Audit Trail de Entidades - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN007-001 | -| **Modulo** | MGN-007 Audit | -| **Sprint** | Sprint 5 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-AUDIT-001 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** tener registro automatico de todos los cambios en entidades criticas -**Para** mantener trazabilidad completa y poder restaurar datos si es necesario - ---- - -## Descripcion - -El sistema registra automaticamente todas las operaciones CRUD en entidades marcadas como auditables. Cada registro incluye quien hizo el cambio, cuando, que campos cambiaron y los valores anteriores/nuevos. Los campos sensibles se enmascaran. - -### Contexto - -- Registro automatico sin intervencion del desarrollador -- Almacenamiento de valores old/new para cada cambio -- Campos sensibles enmascarados -- Posibilidad de restaurar estado anterior - ---- - -## Criterios de Aceptacion - -### Escenario 1: Registro automatico de creacion - -```gherkin -Given una entidad marcada con @Auditable (ej: User) -When un usuario crea un nuevo registro -Then el sistema crea automaticamente un audit log con: - | action | "create" | - | old_values | null | - | new_values | { todos los campos } | - | user_id | ID del usuario que creo | -``` - -### Escenario 2: Registro automatico de actualizacion - -```gherkin -Given un usuario existente con name = "John Doe" -When un administrador cambia el nombre a "John Smith" -Then el sistema crea un audit log con: - | action | "update" | - | old_values | { "name": "John Doe" } | - | new_values | { "name": "John Smith" } | - | changed_fields | ["name"] | - And solo se registran los campos que cambiaron -``` - -### Escenario 3: Registro de eliminacion soft-delete - -```gherkin -Given un registro activo -When un usuario lo elimina (soft delete) -Then el sistema crea un audit log con: - | action | "delete" | - | old_values | { "deleted_at": null } | - | new_values | { "deleted_at": "2025-12-05T..." } | -``` - -### Escenario 4: Consultar historial de entidad - -```gherkin -Given un usuario administrador con permiso "audit.read" -When accede a GET /api/v1/audit/entity/users/{userId} -Then el sistema responde con status 200 - And retorna lista de cambios ordenados por fecha desc - And cada entrada incluye: action, user, changedFields, createdAt -``` - -### Escenario 5: Ver detalle de un cambio - -```gherkin -Given un registro de audit existente -When accede a GET /api/v1/audit/{auditId} -Then el sistema responde con status 200 - And retorna oldValues y newValues completos - And incluye metadata: ipAddress, userAgent, requestId -``` - -### Escenario 6: Campos sensibles enmascarados - -```gherkin -Given una entidad con campo password marcado como sensitiveField -When se cambia el password -Then el audit log registra: - | old_values | { "password": "[REDACTED]" } | - | new_values | { "password": "[REDACTED]" } | - And el valor real nunca se almacena -``` - -### Escenario 7: Restaurar estado anterior - -```gherkin -Given un registro de audit para update de User - And administrador con permiso "audit.restore" -When envia POST /api/v1/audit/{auditId}/restore -Then la entidad se restaura a los valores de old_values - And se crea nuevo audit log con action = "restore" - And el cambio se refleja inmediatamente -``` - -### Escenario 8: Filtrar audit logs - -```gherkin -Given multiples registros de audit -When consulta GET /api/v1/audit?entityType=users&action=update&from=2025-12-01 -Then retorna solo logs que coinciden con todos los filtros - And soporta paginacion -``` - -### Escenario 9: Operacion bulk genera multiples registros - -```gherkin -Given una operacion de actualizacion masiva de 50 usuarios -When se ejecuta la operacion -Then se crean 50 registros de audit individuales - And cada uno tiene su entity_id correspondiente - And comparten el mismo request_id -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| HISTORIAL DE CAMBIOS - Usuario: John Smith | -+------------------------------------------------------------------+ -| [Filtrar por accion v] [Desde: ____] [Hasta: ____] [Buscar] | -+------------------------------------------------------------------+ - -| FECHA | ACCION | USUARIO | CAMPOS | -+------------------------------------------------------------------+ -| 2025-12-05 10:30 | update | Admin | name, email | -| name: "John Doe" -> "John Smith" | -| email: "old@mail.com" -> "new@mail.com" [Ver] [↺ Rest]| -+------------------------------------------------------------------+ -| 2025-12-04 15:00 | update | System | status | -| status: "pending" -> "active" [Ver] | -+------------------------------------------------------------------+ -| 2025-12-01 09:00 | create | HR Manager | (nuevo registro) | -| Registro creado con todos los campos [Ver] | -+------------------------------------------------------------------+ - -Modal: Detalle de Cambio -+------------------------------------------------------------------+ -| DETALLE DEL CAMBIO [X] | -+------------------------------------------------------------------+ -| ID: audit-uuid-123 | -| Fecha: 2025-12-05 10:30:00 | -| Usuario: Admin (admin@company.com) | -| IP: 192.168.1.100 | -| Accion: update | -| | -| CAMPOS MODIFICADOS | -+------------------------------------------------------------------+ -| Campo | Valor Anterior | Valor Nuevo | -+------------------------------------------------------------------+ -| name | John Doe | John Smith | -| email | old@mail.com | new@mail.com | -+------------------------------------------------------------------+ -| | -| [Cerrar] [=== Restaurar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /audit/entity/:type/:id | Historial de entidad | -| GET | /audit/:auditId | Detalle de cambio | -| GET | /audit | Buscar con filtros | -| POST | /audit/:auditId/restore | Restaurar estado | - -### Decorator @Auditable - -```typescript -@Auditable({ - fields: ['name', 'email', 'status', 'role_id'], - sensitiveFields: ['password', 'token'], - enabled: true -}) -@Entity() -export class User extends BaseEntity { ... } -``` - -### Performance - -- Logging asincrono (no bloquea operacion principal) -- Overhead target: < 10ms por operacion -- Indices en: tenant_id, entity_type, entity_id, created_at - ---- - -## Definicion de Done - -- [ ] TypeORM subscriber para capturar cambios -- [ ] Decorator @Auditable funcional -- [ ] Diff de campos modificados -- [ ] Enmascaramiento de campos sensibles -- [ ] Endpoint historial por entidad -- [ ] Endpoint restaurar estado -- [ ] Busqueda con filtros multiples -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla audit_logs | Con particionamiento mensual | -| TypeORM Subscriber | Para interceptar operaciones | -| Auth Module | Para obtener usuario actual | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN007-004 | Reportes de auditoria | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-002.md b/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-002.md deleted file mode 100644 index 6c03426..0000000 --- a/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-002.md +++ /dev/null @@ -1,260 +0,0 @@ -# US-MGN007-002: Gestionar Logs de Acceso - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN007-002 | -| **Modulo** | MGN-007 Audit | -| **Sprint** | Sprint 5 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-AUDIT-002 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** tener registro de todos los accesos a recursos del sistema -**Para** monitorear el uso, detectar anomalias y cumplir con normativas - ---- - -## Descripcion - -El sistema registra automaticamente todas las peticiones HTTP a recursos protegidos, incluyendo informacion del usuario, IP, tiempo de respuesta y resultado. Soporta diferentes niveles de logging configurables. - -### Contexto - -- Logging automatico de peticiones HTTP -- Filtrado de paths irrelevantes -- Sanitizacion de datos sensibles -- Metricas en tiempo real - ---- - -## Criterios de Aceptacion - -### Escenario 1: Registro automatico de peticion - -```gherkin -Given un usuario autenticado -When realiza una peticion POST /api/v1/users -Then el sistema registra automaticamente: - | method | "POST" | - | path | "/api/v1/users" | - | status_code | 201 | - | response_time_ms | 45 | - | user_id | ID del usuario | - | ip_address | IP del cliente | -``` - -### Escenario 2: Excluir paths de logging - -```gherkin -Given paths configurados como excluidos: ["/health", "/metrics"] -When un servicio llama a GET /health -Then el sistema NO registra la peticion - And el endpoint responde normalmente -``` - -### Escenario 3: Consultar logs de acceso - -```gherkin -Given un administrador con permiso "audit.access.read" -When accede a GET /api/v1/audit/access?from=2025-12-01&userId=uuid -Then el sistema responde con status 200 - And retorna lista de accesos filtrados - And soporta paginacion -``` - -### Escenario 4: Consultar metricas de acceso - -```gherkin -Given registros de acceso de la ultima hora -When consulta GET /api/v1/audit/access/metrics?period=1h -Then retorna: - | requestsPerMinute | 83.3 | - | errorRate | 0.02 | - | avgResponseTime | 120 | - | p99ResponseTime | 450 | -``` - -### Escenario 5: Logging de errores - -```gherkin -Given una peticion que resulta en error 500 -When el sistema registra el acceso -Then incluye el status_code 500 - And se marca como error para facil filtrado -``` - -### Escenario 6: Sanitizacion de datos sensibles - -```gherkin -Given nivel de logging = VERBOSE (incluye body) - And peticion POST con { password: "secret123" } -When el sistema registra la peticion -Then el body se guarda como { password: "****" } - And otros campos se mantienen intactos -``` - -### Escenario 7: Geo-localizacion por IP - -```gherkin -Given una peticion desde IP 200.55.128.100 -When el sistema registra el acceso -Then incluye country_code basado en geo-IP - And se puede filtrar por pais -``` - -### Escenario 8: Consultar sesiones de usuario - -```gherkin -Given un usuario con multiples sesiones activas -When consulta GET /api/v1/audit/access/user/{userId}/sessions -Then retorna lista de sesiones con: - | sessionId, startedAt, lastActivityAt, ipAddress, requestCount | -``` - -### Escenario 9: Top endpoints - -```gherkin -Given metricas del periodo -When consulta metricas -Then incluye topEndpoints con los paths mas accedidos - And topErrors con los errores mas frecuentes -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| LOGS DE ACCESO | -+------------------------------------------------------------------+ -| [Periodo: 1h v] [Usuario: ___] [Status: Todos v] [Metodo: Todos v]| -+------------------------------------------------------------------+ - -METRICAS -+------------------------------------------------------------------+ -| Peticiones/min: 83 | Error Rate: 2% | Resp Avg: 120ms | -+------------------------------------------------------------------+ - -| FECHA | METODO | PATH | STATUS | TIEMPO | -+------------------------------------------------------------------+ -| 2025-12-05 10:30 | POST | /api/v1/users | 201 | 45ms | -| 2025-12-05 10:29 | GET | /api/v1/users | 200 | 12ms | -| 2025-12-05 10:28 | DELETE | /api/v1/users/123 | 500 | 230ms | -| 2025-12-05 10:27 | GET | /api/v1/settings | 200 | 8ms | -+------------------------------------------------------------------+ - [< Anterior] [1] [Siguiente >] - -Detalle de Acceso: -+------------------------------------------------------------------+ -| DETALLE DEL ACCESO [X] | -+------------------------------------------------------------------+ -| Request ID: req-uuid-456 | -| Fecha: 2025-12-05 10:30:00 | -| Usuario: admin@company.com | -| | -| REQUEST | -| Metodo: POST | -| Path: /api/v1/users | -| Query: ?notify=true | -| | -| RESPUESTA | -| Status: 201 Created | -| Tiempo: 45ms | -| | -| ORIGEN | -| IP: 192.168.1.100 | -| Pais: MX | -| User-Agent: Mozilla/5.0 (Windows NT 10.0)... | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /audit/access | Listar logs | -| GET | /audit/access/:id | Detalle de acceso | -| GET | /audit/access/metrics | Metricas agregadas | -| GET | /audit/access/user/:userId/sessions | Sesiones de usuario | - -### Middleware de Logging - -```typescript -@Injectable() -export class AccessLogMiddleware implements NestMiddleware { - use(req: Request, res: Response, next: NextFunction) { - const startTime = Date.now(); - - res.on('finish', () => { - const duration = Date.now() - startTime; - this.logAccess(req, res, duration); - }); - - next(); - } -} -``` - -### Niveles de Logging - -| Nivel | Registra | -|-------|----------| -| MINIMAL | Solo errores y autenticacion | -| STANDARD | Todo excepto GETs de listas | -| VERBOSE | Todas las peticiones | -| DEBUG | Todo + request body | - ---- - -## Definicion de Done - -- [ ] Middleware de logging implementado -- [ ] Filtrado de paths excluidos -- [ ] Sanitizacion de body -- [ ] Geo-IP lookup -- [ ] Endpoint de consulta con filtros -- [ ] Metricas agregadas -- [ ] Dashboard de sesiones -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla access_logs | Con particionamiento diario | -| Redis | Para cache de metricas | -| Servicio GeoIP | Para localizacion | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN007-003 | Eventos de seguridad (usa datos) | -| US-MGN007-004 | Reportes de auditoria | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-003.md b/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-003.md deleted file mode 100644 index 2855978..0000000 --- a/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-003.md +++ /dev/null @@ -1,261 +0,0 @@ -# US-MGN007-003: Gestionar Eventos de Seguridad - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN007-003 | -| **Modulo** | MGN-007 Audit | -| **Sprint** | Sprint 5 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-AUDIT-003 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** detectar y gestionar eventos de seguridad automaticamente -**Para** proteger el sistema y responder rapidamente a amenazas - ---- - -## Descripcion - -El sistema detecta automaticamente eventos de seguridad como intentos de login fallidos, accesos desde ubicaciones inusuales y cambios en permisos. Clasifica por severidad y permite flujo de resolucion. - -### Contexto - -- Deteccion automatica de patrones sospechosos -- Clasificacion por severidad -- Alertas en tiempo real -- Flujo de resolucion de incidentes - ---- - -## Criterios de Aceptacion - -### Escenario 1: Detectar brute force - -```gherkin -Given configuracion: max 5 intentos en 15 minutos -When un usuario tiene 5 intentos de login fallidos -Then el sistema crea evento de seguridad: - | event_type | "brute_force_detected" | - | severity | "high" | - | title | "Multiples intentos de login fallidos" | - And notifica a los administradores - And bloquea la cuenta temporalmente -``` - -### Escenario 2: Detectar login desde nueva ubicacion - -```gherkin -Given usuario que siempre accede desde Mexico -When inicia sesion desde un pais diferente por primera vez -Then el sistema crea evento de seguridad: - | event_type | "session_from_new_location" | - | severity | "medium" | - And incluye los detalles de ubicacion en metadata -``` - -### Escenario 3: Listar eventos de seguridad - -```gherkin -Given un administrador con permiso "audit.security.read" -When accede a GET /api/v1/audit/security?severity=high&isResolved=false -Then retorna eventos sin resolver de severidad alta - And ordenados por fecha descendente -``` - -### Escenario 4: Ver dashboard de seguridad - -```gherkin -Given eventos de seguridad en el sistema -When accede a GET /api/v1/audit/security/dashboard -Then retorna: - | summary | { critical: 0, high: 2, medium: 5, low: 20 } | - | unresolvedCount | 7 | - | recentEvents | [...] | - | topAffectedUsers | [...] | -``` - -### Escenario 5: Resolver evento de seguridad - -```gherkin -Given un evento de seguridad no resuelto - And administrador con permiso "audit.security.resolve" -When envia POST /api/v1/audit/security/{eventId}/resolve - | resolution | "verified_legitimate" | - | notes | "Usuario verificado por telefono" | -Then el evento se marca como resuelto - And se registra quien y cuando lo resolvio -``` - -### Escenario 6: Detectar cambio de permisos - -```gherkin -Given un usuario regular -When se le asigna rol de administrador -Then el sistema crea evento de seguridad: - | event_type | "permission_escalation" | - | severity | "medium" | - | metadata | { old_role: "user", new_role: "admin" } | -``` - -### Escenario 7: Detectar eliminacion masiva - -```gherkin -Given configuracion: alertar si mas de 10 deletes en 5 minutos -When un usuario elimina 15 registros en 3 minutos -Then el sistema crea evento de seguridad: - | event_type | "mass_delete_attempt" | - | severity | "high" | - | metadata | { count: 15, windowMinutes: 3 } | -``` - -### Escenario 8: Alertas automaticas - -```gherkin -Given evento critico detectado -When el evento se registra -Then se envia notificacion a todos los admins - And se marca el canal utilizado (email, push) - And se registra a quienes se notifico -``` - -### Escenario 9: Detalle de evento - -```gherkin -Given un evento de seguridad existente -When consulta GET /api/v1/audit/security/{eventId} -Then retorna toda la informacion del evento - And incluye eventos relacionados (si aplica) - And muestra timeline de acciones -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| EVENTOS DE SEGURIDAD [Dashboard]| -+------------------------------------------------------------------+ -| [Severidad: Todas v] [Resueltos: No v] [Tipo: Todos v] [Buscar...] | -+------------------------------------------------------------------+ - -RESUMEN -+------------------------------------------------------------------+ -| ⚫ Critical: 0 | 🔴 High: 2 | 🟠 Medium: 5 | 🟢 Low: 20 | ⏳ 7 pendientes | -+------------------------------------------------------------------+ - -| FECHA | SEVERIDAD | TIPO | USUARIO | ESTADO | -+------------------------------------------------------------------+ -| 2025-12-05 10:30 | 🔴 High | Brute force detected | john@... | ⏳ | -| 5 intentos fallidos en 10 minutos desde IP 192.168.1.100 | -| [Ver] [=== Resolver ===]| -+------------------------------------------------------------------+ -| 2025-12-05 09:15 | 🟠 Medium | Nueva ubicacion | mary@... | ⏳ | -| Login desde Estados Unidos (usualmente Mexico) | -| [Ver] [=== Resolver ===]| -+------------------------------------------------------------------+ -| 2025-12-04 16:00 | 🟠 Medium | Rol modificado | new@... | ✓ | -| Usuario promovido a admin | -| Resuelto por: Admin - "Promocion autorizada por HR" | -+------------------------------------------------------------------+ - -Modal: Resolver Evento -+------------------------------------------------------------------+ -| RESOLVER EVENTO [X] | -+------------------------------------------------------------------+ -| Evento: Multiples intentos de login fallidos | -| Usuario afectado: john@company.com | -| Fecha: 2025-12-05 10:30:00 | -| | -| Resolucion* | -| [v] Verificado como legitimo | -| [ ] Bloquear usuario | -| [ ] Resetear password | -| [ ] Falso positivo | -| | -| Notas | -| [Usuario verificado por telefono. Olvido password. ] | -| | -| [Cancelar] [=== Resolver ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /audit/security | Listar eventos | -| GET | /audit/security/:id | Detalle de evento | -| GET | /audit/security/dashboard | Dashboard | -| POST | /audit/security/:id/resolve | Resolver evento | - -### Tipos de Eventos - -| Categoria | Eventos | -|-----------|---------| -| Autenticacion | login_failed, brute_force_detected, login_blocked | -| Sesiones | session_from_new_location, concurrent_session | -| Permisos | permission_escalation, admin_created | -| Datos | mass_delete_attempt, data_export, sensitive_data_access | - -### Severidades - -| Nivel | Accion | -|-------|--------| -| low | Solo log | -| medium | Notificar admin | -| high | Notificar + considerar bloqueo | -| critical | Bloquear + notificar urgente | - ---- - -## Definicion de Done - -- [ ] Detector de patrones (brute force, etc.) -- [ ] Clasificacion por severidad -- [ ] Creacion automatica de eventos -- [ ] Alertas a administradores -- [ ] Dashboard de seguridad -- [ ] Flujo de resolucion -- [ ] Integracion con bloqueo de cuentas -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN007-002 | Logs de acceso (datos de entrada) | -| MGN-008 Notifications | Para alertas | -| Tabla security_events | Storage | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN007-004 | Reportes de seguridad | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-004.md b/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-004.md deleted file mode 100644 index 35bff45..0000000 --- a/docs/02-fase-core-business/MGN-007-audit/historias-usuario/US-MGN007-004.md +++ /dev/null @@ -1,303 +0,0 @@ -# US-MGN007-004: Consultar y Generar Reportes de Auditoria - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN007-004 | -| **Modulo** | MGN-007 Audit | -| **Sprint** | Sprint 5 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 6 | -| **Estado** | Ready | -| **RF Relacionado** | RF-AUDIT-004 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** buscar en logs y generar reportes de auditoria -**Para** realizar analisis forense, cumplir con normativas y monitorear operaciones - ---- - -## Descripcion - -El sistema proporciona busqueda avanzada en todos los tipos de logs, generacion de reportes en multiples formatos, reportes programados por email y dashboard de metricas. Incluye archivado automatico de logs antiguos. - -### Contexto - -- Busqueda avanzada con multiples filtros -- Exportacion a CSV, XLSX, PDF, JSON -- Reportes programados automaticos -- Dashboard con metricas en tiempo real -- Archivado para cumplimiento normativo - ---- - -## Criterios de Aceptacion - -### Escenario 1: Busqueda avanzada en logs - -```gherkin -Given un administrador con permiso "audit.search" -When envia POST /api/v1/audit/search con filtros: - | from | "2025-12-01" | - | to | "2025-12-05" | - | entityType | "users" | - | action | ["update", "delete"] | -Then retorna logs que coinciden con todos los filtros - And incluye meta con total, page, queryTime -``` - -### Escenario 2: Busqueda de texto libre - -```gherkin -Given logs con diferentes descripciones -When busca con searchText = "email change" -Then retorna logs donde description o metadata contienen el texto - And soporta busqueda case-insensitive -``` - -### Escenario 3: Exportar a CSV - -```gherkin -Given resultados de busqueda -When solicita exportar con format = "csv" - And cantidad <= 100,000 registros -Then el sistema genera archivo CSV - And retorna downloadUrl con expiracion -``` - -### Escenario 4: Exportar a PDF - -```gherkin -Given resultados de busqueda -When solicita exportar con format = "pdf" - And cantidad > 1,000 registros -Then el sistema responde con error - And indica limite de 1,000 para PDF -``` - -### Escenario 5: Generar reporte asincrono - -```gherkin -Given una busqueda con muchos resultados -When solicita POST /api/v1/audit/reports -Then retorna: - | jobId | "uuid" | - | status | "processing" | - | estimatedTime | 30 | - And se puede consultar progreso con GET /reports/:jobId -``` - -### Escenario 6: Crear reporte programado - -```gherkin -Given administrador con permiso "audit.reports.schedule" -When crea POST /api/v1/audit/reports/scheduled - | name | "Reporte Semanal" | - | schedule | "0 8 * * 1" | - | recipients | ["admin@company.com"] | -Then el reporte se guarda - And se calcula nextRunAt = proximo lunes 8am -``` - -### Escenario 7: Ejecutar reporte programado - -```gherkin -Given reporte programado activo -When llega la hora programada -Then el sistema genera el reporte - And lo envia por email a los recipients - And actualiza lastRunAt y nextRunAt -``` - -### Escenario 8: Ver dashboard de auditoria - -```gherkin -Given logs de diferentes tipos -When accede a GET /api/v1/audit/dashboard -Then retorna: - | totalRecords | numero total | - | recordsToday | registros de hoy | - | topUsers | usuarios mas activos | - | activityTrend | tendencia por fecha | -``` - -### Escenario 9: Configurar alerta de auditoria - -```gherkin -Given administrador con permiso "audit.alerts.manage" -When crea alerta: - | condition | { logType: "audit", action: "delete", threshold: 100 } | - | windowMinutes | 5 | - | notify | ["admin@company.com"] | -Then la alerta se activa - And notifica si hay > 100 deletes en 5 minutos -``` - -### Escenario 10: Consultar politicas de retencion - -```gherkin -Given diferentes tipos de logs -When consulta politicas de retencion -Then muestra: - | Audit Trail | 1 año online, 7 años archivo | - | Access Logs | 90 dias online, 1 año archivo | - | Security Events | 2 años online, 7 años archivo | -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| BUSQUEDA DE AUDITORIA | -+------------------------------------------------------------------+ - -FILTROS -+------------------------------------------------------------------+ -| Desde: [2025-12-01] Hasta: [2025-12-05] | -| Tipo de Log: [Audit Trail v] Tipo Entidad: [users v] | -| Acciones: [✓ Create] [✓ Update] [✓ Delete] [ ] Restore | -| Usuario: [_______________] IP: [_______________] | -| Buscar: [email change________________________] | -| | -| [Limpiar] [=== Buscar ===] | -+------------------------------------------------------------------+ - -RESULTADOS (250 encontrados, 45ms) -+------------------------------------------------------------------+ -| [Exportar: CSV v] [Crear Reporte Programado] | -+------------------------------------------------------------------+ -| FECHA | TIPO | ENTIDAD | ACCION | USUARIO | -+------------------------------------------------------------------+ -| 2025-12-05 10:30 | audit | users | update | admin@... | -| 2025-12-05 10:00 | audit | users | delete | hr@... | -| 2025-12-04 15:00 | audit | contacts | update | sales@... | -+------------------------------------------------------------------+ - [< Anterior] [1] [Siguiente >] - -Modal: Crear Reporte Programado -+------------------------------------------------------------------+ -| NUEVO REPORTE PROGRAMADO [X] | -+------------------------------------------------------------------+ -| Nombre* | -| [Reporte Semanal de Actividad ] | -| | -| Tipo de Reporte: [Actividad v] | -| Formato: ( ) CSV (•) XLSX ( ) PDF | -| | -| Programacion | -| Frecuencia: [Semanal v] Dia: [Lunes v] Hora: [08:00 v] | -| | -| Destinatarios* | -| [admin@company.com ] [+]| -| [auditor@company.com ] [x]| -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ - -DASHBOARD -+------------------------------------------------------------------+ -| METRICAS DE AUDITORIA [Periodo: 7d v]| -+------------------------------------------------------------------+ -| | -| +-------------+ +-------------+ +-------------+ +-------------+| -| | Total Logs | | Hoy | | Usuarios | | Eventos Seg || -| | 125,430 | | 1,234 | | 45 activos | | 7 pendiente || -| +-------------+ +-------------+ +-------------+ +-------------+| -| | -| [GRAFICO: Actividad por dia - ultimos 7 dias] | -| ^ | -| 1500| ___ | -| 1000|___ / \___ | -| 500| \___ | -| +---+---+---+---+---+---+---+ | -| L M M J V S D | -| | -| TOP USUARIOS | TOP ENTIDADES | -| 1. admin@... (234 acciones) | 1. users (450 cambios) | -| 2. hr@... (156 acciones) | 2. contacts (320 cambios) | -| 3. sales@... (89 acciones) | 3. settings (45 cambios) | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| POST | /audit/search | Busqueda avanzada | -| POST | /audit/reports | Generar reporte | -| GET | /audit/reports/:jobId | Estado de reporte | -| POST | /audit/reports/scheduled | Crear programado | -| GET | /audit/reports/scheduled | Listar programados | -| GET | /audit/dashboard | Dashboard metricas | -| POST | /audit/alerts | Crear alerta | - -### Limites de Exportacion - -| Formato | Limite | -|---------|--------| -| CSV | 100,000 registros | -| XLSX | 50,000 registros | -| PDF | 1,000 registros | -| JSON | 100,000 registros | - -### Retencion - -| Tipo | Online | Archivo | -|------|--------|---------| -| Audit Trail | 1 año | 7 años (S3/Glacier) | -| Access Logs | 90 dias | 1 año | -| Security Events | 2 años | 7 años | - ---- - -## Definicion de Done - -- [ ] Busqueda avanzada con multiples filtros -- [ ] Exportacion CSV, XLSX, PDF, JSON -- [ ] Jobs asincronos para reportes grandes -- [ ] Reportes programados con cron -- [ ] Envio por email -- [ ] Dashboard con metricas -- [ ] Sistema de alertas configurables -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN007-001 | Audit trail (datos) | -| US-MGN007-002 | Access logs (datos) | -| US-MGN007-003 | Security events (datos) | -| Bull Queue | Para jobs asincronos | -| MGN-008 Notifications | Para envio email | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Ninguno | Modulo final de audit | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-001.md b/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-001.md deleted file mode 100644 index a725af3..0000000 --- a/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-001.md +++ /dev/null @@ -1,268 +0,0 @@ -# US-MGN008-001: Gestionar Notificaciones In-App - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN008-001 | -| **Modulo** | MGN-008 Notifications | -| **Sprint** | Sprint 5 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-NOTIF-001 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** recibir notificaciones dentro de la aplicacion en tiempo real -**Para** estar informado de eventos importantes sin salir de mi flujo de trabajo - ---- - -## Descripcion - -El sistema muestra notificaciones in-app en tiempo real usando WebSockets. Los usuarios pueden ver, marcar como leidas, archivar y ejecutar acciones desde las notificaciones. Soporta diferentes tipos, categorias y prioridades. - -### Contexto - -- Entrega en tiempo real via WebSocket -- Diferentes tipos y categorias -- Acciones ejecutables desde notificacion -- Agrupacion de notificaciones similares - ---- - -## Criterios de Aceptacion - -### Escenario 1: Recibir notificacion en tiempo real - -```gherkin -Given un usuario con sesion activa - And conectado al WebSocket de notificaciones -When el sistema envia una notificacion a ese usuario -Then la notificacion aparece instantaneamente en la UI - And el contador de no leidas se incrementa - And se muestra toast/popup segun prioridad -``` - -### Escenario 2: Listar mis notificaciones - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/notifications -Then el sistema responde con status 200 - And retorna lista de notificaciones ordenadas por fecha desc - And incluye unreadCount en la respuesta -``` - -### Escenario 3: Filtrar por estado de lectura - -```gherkin -Given un usuario con notificaciones leidas y no leidas -When consulta GET /api/v1/notifications?isRead=false -Then retorna solo las notificaciones no leidas -``` - -### Escenario 4: Marcar como leida - -```gherkin -Given una notificacion no leida -When envia PUT /api/v1/notifications/{id}/read -Then la notificacion se marca como leida - And se actualiza read_at - And el contador disminuye -``` - -### Escenario 5: Marcar todas como leidas - -```gherkin -Given multiples notificaciones no leidas -When envia PUT /api/v1/notifications/read-all -Then todas las notificaciones se marcan como leidas - And el contador se resetea a 0 -``` - -### Escenario 6: Ejecutar accion desde notificacion - -```gherkin -Given notificacion de tipo "approval_requested" - And contiene accion "approve" -When envia POST /api/v1/notifications/{id}/actions/approve -Then la accion se ejecuta (aprueba la solicitud) - And la notificacion se marca como leida - And se muestra mensaje de confirmacion -``` - -### Escenario 7: Obtener contador de no leidas - -```gherkin -Given un usuario con notificaciones -When consulta GET /api/v1/notifications/count -Then retorna: - | total | 50 | - | unread | 5 | - | byCategory | { info: 3, warning: 2 } | -``` - -### Escenario 8: Agrupacion de notificaciones similares - -```gherkin -Given 5 notificaciones de tipo "document_comment" para el mismo documento en 1 hora -When se muestran al usuario -Then se agrupan en una sola notificacion: - | title | "5 nuevos comentarios" | - | message | "En el documento 'Propuesta Q1'" | -``` - -### Escenario 9: Archivar notificacion - -```gherkin -Given una notificacion visible -When envia PUT /api/v1/notifications/{id}/archive -Then la notificacion se archiva - And no aparece en la lista normal - And sigue accesible con filtro isArchived=true -``` - -### Escenario 10: Notificacion urgente muestra popup - -```gherkin -Given notificacion con priority = "urgent" -When llega al usuario -Then se muestra popup modal que requiere interaccion - And no desaparece automaticamente -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| HEADER [🔔 5] [Usuario v] | -+------------------------------------------------------------------+ - -Panel de Notificaciones (dropdown): -+------------------------------------------------------------------+ -| NOTIFICACIONES [Marcar todas leidas]| -+------------------------------------------------------------------+ -| 🔵 Nueva tarea asignada hace 5 min | -| Te han asignado la tarea 'Revisar informe' | -| [Ver tarea] [Archivar] | -+------------------------------------------------------------------+ -| 🔵 Aprobacion pendiente hace 15 min | -| Pedido #1234 requiere tu aprobacion | -| [Aprobar] [Rechazar] [Ver] [Arch] | -+------------------------------------------------------------------+ -| 🟠 Stock bajo hace 1 hora | -| Producto 'Widget A' tiene stock critico | -| [Ver producto] [Arch] | -+------------------------------------------------------------------+ -| 5 nuevos comentarios hace 2 horas | -| En el documento 'Propuesta Q1' | -| [Ver documento] [Arch] | -+------------------------------------------------------------------+ -| [Ver todas las notificaciones] | -+------------------------------------------------------------------+ - -Leyenda: -🔵 = No leida (circulo azul) -🟠 = Warning (circulo naranja) -Sin icono = Ya leida - -Pagina Completa de Notificaciones: -+------------------------------------------------------------------+ -| MIS NOTIFICACIONES | -+------------------------------------------------------------------+ -| [Todas v] [No leidas] [Archivadas] [Buscar...] | -+------------------------------------------------------------------+ - -| FECHA | TIPO | MENSAJE | ESTADO | -+------------------------------------------------------------------+ -| 2025-12-05 10:30 | Tarea | Nueva tarea... | 🔵 | -| 2025-12-05 10:15 | Aprobacion | Pedido #1234... | 🔵 | -| 2025-12-05 09:00 | Stock | Widget A bajo... | 🟠 | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /notifications | Listar mis notificaciones | -| GET | /notifications/count | Contador | -| PUT | /notifications/:id/read | Marcar como leida | -| PUT | /notifications/read-all | Marcar todas | -| PUT | /notifications/:id/archive | Archivar | -| POST | /notifications/:id/actions/:actionId | Ejecutar accion | - -### WebSocket Events - -```typescript -// Cliente conecta -ws://api.example.com/notifications?token=JWT - -// Eventos recibidos -interface NotificationEvent { - type: 'new' | 'read' | 'archived' | 'count_update'; - payload: Notification | { count: number }; -} -``` - -### Categorias - -| Categoria | Color | Uso | -|-----------|-------|-----| -| info | Azul | Informativo | -| success | Verde | Confirmacion | -| warning | Amarillo | Atencion | -| error | Rojo | Problema | - ---- - -## Definicion de Done - -- [ ] Gateway WebSocket funcional -- [ ] CRUD notificaciones -- [ ] Entrega en tiempo real -- [ ] Marcar como leida individual/masivo -- [ ] Ejecutar acciones desde notificacion -- [ ] Agrupacion de similares -- [ ] Contador en header -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla notifications | Storage | -| Socket.io Gateway | WebSocket | -| Redis | Para pub/sub | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| UI Header | Badge de notificaciones | -| Todos los modulos | Pueden enviar notificaciones | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-002.md b/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-002.md deleted file mode 100644 index 7ef4506..0000000 --- a/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-002.md +++ /dev/null @@ -1,292 +0,0 @@ -# US-MGN008-002: Gestionar Notificaciones por Email - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN008-002 | -| **Modulo** | MGN-008 Notifications | -| **Sprint** | Sprint 5 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-NOTIF-002 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** gestionar el envio de emails transaccionales con templates -**Para** comunicar eventos importantes a los usuarios de forma profesional - ---- - -## Descripcion - -El sistema envia emails transaccionales usando templates con variables. Los templates son personalizables por tenant (branding). Los emails se procesan via cola con reintentos y tracking opcional. - -### Contexto - -- Templates con sintaxis Handlebars -- Personalizacion por tenant (logo, colores) -- Cola de envio con reintentos -- Tracking de apertura y clicks - ---- - -## Criterios de Aceptacion - -### Escenario 1: Enviar email usando template - -```gherkin -Given un template "welcome" configurado -When sistema envia POST /api/v1/notifications/email - | template | "welcome" | - | to | ["user@example.com"] | - | variables | { user: { firstName: "Juan" } } | -Then el email se encola para envio - And retorna jobId para tracking -``` - -### Escenario 2: Preview de template - -```gherkin -Given un template con variables -When envia POST /api/v1/notifications/email/templates/{id}/preview - | variables | { user: { firstName: "Juan" } } | -Then retorna el email renderizado sin enviarlo - And muestra subject y body con las variables reemplazadas -``` - -### Escenario 3: Listar templates disponibles - -```gherkin -Given un administrador de tenant -When accede a GET /api/v1/notifications/email/templates -Then retorna templates globales + templates del tenant - And cada template indica sus variables disponibles -``` - -### Escenario 4: Crear template personalizado - -```gherkin -Given un administrador con permiso "notifications.templates.manage" -When crea POST /api/v1/notifications/email/templates - | code | "custom_order_confirmation" | - | subject | "Pedido {{order.number}} confirmado" | - | bodyHtml | "..." | -Then el template se guarda para el tenant - And queda disponible para envios -``` - -### Escenario 5: Reintento automatico en fallo - -```gherkin -Given un email encolado -When el envio falla (ej: SMTP timeout) -Then el sistema reintenta hasta 3 veces - And espera tiempo exponencial entre reintentos - And marca como failed si todos fallan -``` - -### Escenario 6: Email con branding de tenant - -```gherkin -Given tenant con configuracion: - | logoUrl | "https://company.com/logo.png" | - | primaryColor | "#FF5733" | -When se envia un email -Then el template incluye el logo del tenant - And los colores corresponden a la marca -``` - -### Escenario 7: Consultar historial de envios - -```gherkin -Given emails enviados -When consulta GET /api/v1/notifications/email/history?to=user@example.com -Then retorna lista de emails enviados a ese destinatario - And incluye status, sentAt, opened, openedAt -``` - -### Escenario 8: Tracking de apertura - -```gherkin -Given email enviado con tracking habilitado -When el usuario abre el email -Then el sistema registra openedAt y openCount - And registra userAgent e IP (si disponible) -``` - -### Escenario 9: Email con adjuntos - -```gherkin -Given necesidad de enviar factura adjunta -When envia email con attachments - | filename | "factura.pdf" | - | content | base64 o URL | -Then el email incluye el adjunto -``` - -### Escenario 10: Programar envio - -```gherkin -Given email a enviar en el futuro -When especifica scheduledAt -Then el email no se envia inmediatamente - And se procesa a la hora indicada -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| TEMPLATES DE EMAIL [+ Nuevo Template]| -+------------------------------------------------------------------+ -| [Categoria: Todas v] [Origen: Todos v] [Buscar...] | -+------------------------------------------------------------------+ - -| CODIGO | NOMBRE | CATEGORIA | ORIGEN | -+------------------------------------------------------------------+ -| welcome | Email de Bienvenida | Auth | Global | -| password_reset | Recuperar Password | Auth | Global | -| order_created | Pedido Creado | Orders | Global | -| custom_invoice | Factura Personalizada | Financial | Tenant | -+------------------------------------------------------------------+ - -Editor de Template: -+------------------------------------------------------------------+ -| EDITAR TEMPLATE: order_created [X] | -+------------------------------------------------------------------+ -| Nombre: Pedido Creado | -| Codigo: order_created (no editable) | -| | -| Asunto* | -| [Nuevo pedido {{order.number}} recibido ] | -| | -| Variables disponibles: user.firstName, order.number, order.total | -| | -| Cuerpo HTML* | -| +--------------------------------------------------------------+ | -| |

Hola {{user.firstName}},

| | -| |

Hemos recibido tu pedido {{order.number}}.

| | -| | | | -| | | | -| |
Total:{{order.total}}
| | -| +--------------------------------------------------------------+ | -| | -| [Preview] | -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ - -Preview Modal: -+------------------------------------------------------------------+ -| PREVIEW [X] | -+------------------------------------------------------------------+ -| De: Mi Empresa | -| Para: usuario@ejemplo.com | -| Asunto: Nuevo pedido ORD-001 recibido | -+------------------------------------------------------------------+ -| | -| [LOGO DE LA EMPRESA] | -| | -| Hola Juan, | -| | -| Hemos recibido tu pedido ORD-001. | -| | -| Total: $1,500.00 | -| | -| [Ver Pedido] | -| | -| -- | -| Mi Empresa SA | -| contacto@empresa.com | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| POST | /notifications/email | Enviar email | -| GET | /notifications/email/templates | Listar templates | -| POST | /notifications/email/templates | Crear template | -| PUT | /notifications/email/templates/:id | Actualizar | -| POST | /notifications/email/templates/:id/preview | Preview | -| GET | /notifications/email/history | Historial | - -### Variables de Sistema - -```typescript -const systemVariables = { - appName: 'Mi ERP', - appUrl: 'https://app.example.com', - currentYear: new Date().getFullYear(), - supportEmail: 'soporte@example.com' -}; -``` - -### Cola de Emails (Bull) - -```typescript -@Processor('emails') -export class EmailProcessor { - @Process() - async handleEmail(job: Job) { - // Renderizar template - // Enviar via SMTP - // Registrar resultado - } -} -``` - ---- - -## Definicion de Done - -- [ ] Configuracion SMTP funcional -- [ ] CRUD templates con Handlebars -- [ ] Branding por tenant -- [ ] Cola con reintentos -- [ ] Preview de templates -- [ ] Tracking de apertura -- [ ] Historial de envios -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla email_templates | Templates | -| Tabla email_jobs | Cola | -| Bull Queue | Procesamiento asincrono | -| SMTP | Configuracion de envio | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Auth Module | Emails de welcome, reset | -| Todos los modulos | Emails transaccionales | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-003.md b/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-003.md deleted file mode 100644 index 2472806..0000000 --- a/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-003.md +++ /dev/null @@ -1,281 +0,0 @@ -# US-MGN008-003: Gestionar Push Notifications - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN008-003 | -| **Modulo** | MGN-008 Notifications | -| **Sprint** | Sprint 5 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 5 | -| **Estado** | Ready | -| **RF Relacionado** | RF-NOTIF-003 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** recibir notificaciones push en mi navegador o dispositivo movil -**Para** estar alertado de eventos importantes incluso cuando no tengo la app abierta - ---- - -## Descripcion - -El sistema envia notificaciones push a navegadores web (Web Push con VAPID) y dispositivos moviles (FCM/APNs). Los usuarios pueden registrar multiples dispositivos y configurar preferencias de push. - -### Contexto - -- Web Push para navegadores -- FCM para Android -- APNs para iOS -- Sincronizacion con notificaciones in-app - ---- - -## Criterios de Aceptacion - -### Escenario 1: Registrar dispositivo web - -```gherkin -Given un usuario autenticado en navegador -When acepta recibir notificaciones push - And envia POST /api/v1/notifications/push/subscribe con subscription -Then el dispositivo se registra - And retorna vapidPublicKey si es necesaria -``` - -### Escenario 2: Recibir push notification - -```gherkin -Given un usuario con dispositivo registrado - And evento que dispara notificacion -When el sistema envia push -Then el navegador/dispositivo muestra la notificacion - And incluye titulo, cuerpo e icono -``` - -### Escenario 3: Click en push abre la app - -```gherkin -Given push notification recibida -When usuario hace click en ella -Then el navegador abre la app en la URL especificada - And la notificacion in-app correspondiente se marca como leida -``` - -### Escenario 4: Listar mis dispositivos - -```gherkin -Given un usuario con multiples dispositivos registrados -When consulta GET /api/v1/notifications/push/devices -Then retorna lista de dispositivos: - | id, deviceType, deviceName, isActive, lastUsedAt | -``` - -### Escenario 5: Desregistrar dispositivo - -```gherkin -Given un dispositivo registrado -When envia DELETE /api/v1/notifications/push/subscribe/{id} -Then el dispositivo se elimina - And ya no recibe notificaciones push -``` - -### Escenario 6: Respetar quiet hours - -```gherkin -Given usuario con quiet hours 22:00-08:00 -When evento ocurre a las 23:00 -Then el push NO se envia - And la notificacion in-app si se crea -``` - -### Escenario 7: No enviar push si app activa - -```gherkin -Given usuario con app abierta (WebSocket conectado) -When llega una notificacion -Then solo se muestra in-app - And NO se envia push -``` - -### Escenario 8: Enviar push a todos los dispositivos - -```gherkin -Given usuario con 3 dispositivos registrados -When llega notificacion importante -Then se envia push a los 3 dispositivos - And el primer click en cualquiera marca como leida -``` - -### Escenario 9: Token expirado se limpia - -```gherkin -Given un dispositivo con token invalido -When el sistema intenta enviar push - And recibe error de token invalido -Then el dispositivo se marca como inactivo - And se notifica al usuario que reactive push -``` - -### Escenario 10: Broadcast a grupo de usuarios - -```gherkin -Given administrador con permiso -When envia POST /api/v1/notifications/push/broadcast - | tenantId | uuid | - | title | "Mantenimiento programado" | - | filter | { roles: ["admin"] } | -Then se envia push a todos los admins del tenant -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| MIS DISPOSITIVOS | -+------------------------------------------------------------------+ - -DISPOSITIVOS REGISTRADOS -+------------------------------------------------------------------+ -| DISPOSITIVO | TIPO | ULTIMO USO | ACCIONES | -+------------------------------------------------------------------+ -| Chrome en Windows | 🌐 Web | 2025-12-05 10:30 | [Desactivar] | -| Firefox en MacOS | 🌐 Web | 2025-12-04 15:00 | [Desactivar] | -| iPhone de Juan | 📱 iOS | 2025-12-05 08:00 | [Desactivar] | -+------------------------------------------------------------------+ - -[+ Agregar este dispositivo] - -CONFIGURACION DE PUSH -+------------------------------------------------------------------+ -| ✓ Recibir notificaciones push | -| | -| Horas de silencio: | -| [✓] Activar Desde: [22:00] Hasta: [08:00] | -| | -| [=== Guardar ===] | -+------------------------------------------------------------------+ - -Browser Push Permission: -+------------------------------------------------------------------+ -| | -| [LOGO APP] | -| | -| Mi ERP quiere enviarte notificaciones | -| | -| Recibe alertas importantes incluso cuando | -| no tienes la aplicacion abierta. | -| | -| [No, gracias] [=== Permitir ===] | -| | -+------------------------------------------------------------------+ - -Push Notification (Browser): -+------------------------------------------------------------------+ -| [LOGO] Mi ERP | -| Nuevo pedido recibido | -| Pedido #1234 por $1,500.00 de Juan Perez | -| [Ver Pedido] [Descartar] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| POST | /notifications/push/subscribe | Registrar dispositivo | -| DELETE | /notifications/push/subscribe/:id | Desregistrar | -| GET | /notifications/push/devices | Mis dispositivos | -| POST | /notifications/push/send | Enviar push (admin) | -| POST | /notifications/push/broadcast | Broadcast (admin) | - -### Web Push VAPID - -```typescript -// Configuracion -interface VAPIDConfig { - subject: 'mailto:admin@example.com'; - publicKey: 'BG...'; - privateKey: 'encrypted...'; -} - -// Subscription del browser -interface PushSubscription { - endpoint: string; - keys: { - p256dh: string; - auth: string; - }; -} -``` - -### Service Worker - -```typescript -// sw.js -self.addEventListener('push', (event) => { - const data = event.data.json(); - self.registration.showNotification(data.title, { - body: data.body, - icon: data.icon, - data: { url: data.data.url } - }); -}); - -self.addEventListener('notificationclick', (event) => { - event.notification.close(); - clients.openWindow(event.notification.data.url); -}); -``` - ---- - -## Definicion de Done - -- [ ] Web Push con VAPID funcional -- [ ] Service Worker implementado -- [ ] Registro/desregistro de dispositivos -- [ ] Sincronizacion con in-app -- [ ] Quiet hours respetadas -- [ ] Click abre URL correcta -- [ ] Limpieza de tokens invalidos -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla push_subscriptions | Dispositivos | -| US-MGN008-001 | Notificaciones in-app | -| web-push library | Para Web Push | -| Service Worker | En frontend | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Alertas criticas | Push inmediato | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-004.md b/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-004.md deleted file mode 100644 index 284d54b..0000000 --- a/docs/02-fase-core-business/MGN-008-notifications/historias-usuario/US-MGN008-004.md +++ /dev/null @@ -1,300 +0,0 @@ -# US-MGN008-004: Gestionar Preferencias de Notificacion - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN008-004 | -| **Modulo** | MGN-008 Notifications | -| **Sprint** | Sprint 5 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 4 | -| **Estado** | Ready | -| **RF Relacionado** | RF-NOTIF-004 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** configurar mis preferencias de notificaciones -**Para** recibir solo las notificaciones que me interesan por los canales que prefiero - ---- - -## Descripcion - -Cada usuario configura canales habilitados, tipos de notificaciones, horarios de silencio y frecuencia de resumenes. Los emails incluyen link de unsubscribe sin necesidad de login. - -### Contexto - -- Configuracion por canal (in-app, email, push) -- Configuracion por tipo de notificacion -- Quiet hours con timezone -- Digest diario/semanal - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver mis preferencias - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/notifications/preferences -Then retorna su configuracion actual: - | channels | { inApp: true, email: true, push: true } | - | byType | { task_assigned: { enabled: true, channels: [...] } } | - | quietHours | { enabled: false } | - | digest | { enabled: true, frequency: "daily" } | -``` - -### Escenario 2: Actualizar preferencias - -```gherkin -Given un usuario autenticado -When envia PATCH /api/v1/notifications/preferences - | channels.push | false | -Then las preferencias se actualizan - And ya no recibe push notifications -``` - -### Escenario 3: Configurar canales por tipo - -```gherkin -Given tipos de notificacion disponibles -When configura: - | task_assigned | channels: ["inApp", "email"] | - | order_created | channels: ["inApp"] | -Then las tareas llegan por in-app y email - And los pedidos solo por in-app -``` - -### Escenario 4: Configurar quiet hours - -```gherkin -Given usuario que no quiere interrupciones de noche -When configura: - | quietHours.enabled | true | - | quietHours.start | "22:00" | - | quietHours.end | "08:00" | - | quietHours.timezone | "America/Mexico_City" | -Then no recibe push ni sonidos en ese horario - And las notificaciones in-app si se crean silenciosamente -``` - -### Escenario 5: Configurar digest - -```gherkin -Given usuario que prefiere resumen -When configura: - | digest.enabled | true | - | digest.frequency | "daily" | - | digest.time | "09:00" | -Then recibe email resumen cada dia a las 9am - And solo si hubo notificaciones nuevas -``` - -### Escenario 6: Tipos no deshabilitables - -```gherkin -Given tipo "system_alert" marcado como mandatory -When usuario intenta deshabilitar ese tipo -Then el sistema responde con error 400 - And indica que el tipo no puede deshabilitarse -``` - -### Escenario 7: Unsubscribe sin login - -```gherkin -Given email con link de unsubscribe -When usuario hace click en el link - | token | signed-token | - | type | "order_created" | -Then la pagina muestra formulario de confirmacion - And al confirmar, se deshabilita ese tipo de email - And no requiere login -``` - -### Escenario 8: Ver tipos disponibles - -```gherkin -Given un usuario autenticado -When consulta GET /api/v1/notifications/types -Then retorna lista de tipos con: - | type, name, description, availableChannels, canDisable | -``` - -### Escenario 9: Nuevos usuarios usan defaults - -```gherkin -Given tenant con defaults configurados -When se crea un nuevo usuario -Then sus preferencias se inicializan con los defaults del tenant -``` - -### Escenario 10: Digest semanal - -```gherkin -Given usuario con digest semanal los lunes -When es lunes a la hora configurada - And hubo notificaciones en la semana -Then recibe email de resumen semanal - And agrupa por categoria/tipo -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| PREFERENCIAS DE NOTIFICACIONES | -+------------------------------------------------------------------+ - -CANALES -+------------------------------------------------------------------+ -| [✓] Notificaciones in-app | -| [✓] Notificaciones por email | -| [✓] Notificaciones push | -+------------------------------------------------------------------+ - -POR TIPO DE NOTIFICACION -+------------------------------------------------------------------+ -| TIPO | HABILITADO | IN-APP | EMAIL | PUSH | -+------------------------------------------------------------------+ -| Tareas | -| Tarea asignada [✓] [✓] [✓] [✓] | -| Tarea vencida [✓] [✓] [✓] [✓] | -| Aprobaciones | -| Solicitud [✓] [✓] [✓] [✓] | -| Pedidos | -| Nuevo pedido [✓] [✓] [✓] [ ] | -| Pago recibido [✓] [✓] [✓] [ ] | -| Sistema | -| Alertas [🔒] [✓] [✓] [✓] [✓] (no editable)| -+------------------------------------------------------------------+ - -HORAS DE SILENCIO -+------------------------------------------------------------------+ -| [✓] Activar horas de silencio | -| | -| Desde: [22:00 v] Hasta: [08:00 v] | -| Zona horaria: [America/Mexico_City v] | -| | -| Dias: [✓ L] [✓ M] [✓ M] [✓ J] [✓ V] [✓ S] [✓ D] | -| | -| Nota: Las alertas criticas ignoran este horario | -+------------------------------------------------------------------+ - -RESUMEN (DIGEST) -+------------------------------------------------------------------+ -| [✓] Recibir email de resumen | -| | -| Frecuencia: (•) Diario ( ) Semanal | -| Hora de envio: [09:00 v] | -| Dia de semana: [Lunes v] (solo para semanal) | -+------------------------------------------------------------------+ - - [Restaurar defaults] [=== Guardar ===] - -Pagina de Unsubscribe (sin login): -+------------------------------------------------------------------+ -| | -| [LOGO] | -| | -| Confirmar desuscripcion | -| | -| Estas a punto de dejar de recibir emails de: | -| "Nuevos pedidos" | -| | -| Email: usuario@ejemplo.com | -| | -| ( ) Solo este tipo de emails | -| ( ) Todos los emails (excepto sistema) | -| | -| [Cancelar] [=== Confirmar ===] | -| | -| Puedes cambiar tus preferencias en cualquier momento | -| desde tu perfil en la aplicacion. | -| | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /notifications/preferences | Obtener preferencias | -| PATCH | /notifications/preferences | Actualizar | -| GET | /notifications/types | Tipos disponibles | -| POST | /notifications/unsubscribe | Desuscribir (sin auth) | - -### Estructura de Preferencias - -```typescript -interface NotificationPreferences { - channels: { inApp: boolean; email: boolean; push: boolean }; - byType: Record; - quietHours: { enabled: boolean; start: string; end: string; timezone: string }; - digest: { enabled: boolean; frequency: 'daily' | 'weekly'; time: string }; -} -``` - -### Token de Unsubscribe - -```typescript -// Token firmado con JWT o similar -interface UnsubscribeToken { - userId: UUID; - type: string | 'all'; - exp: number; // 7 dias -} -``` - ---- - -## Definicion de Done - -- [ ] CRUD preferencias de usuario -- [ ] Configuracion por canal y tipo -- [ ] Quiet hours con timezone -- [ ] Digest diario/semanal -- [ ] Job de envio de digest -- [ ] Unsubscribe sin login -- [ ] Defaults por tenant -- [ ] UI de preferencias -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla notification_preferences | Storage | -| US-MGN008-001 | In-app para filtrar | -| US-MGN008-002 | Email para digest | -| US-MGN008-003 | Push para filtrar | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Todos los envios | Respetan preferencias | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-009-reports/_MAP.md b/docs/02-fase-core-business/MGN-009-reports/_MAP.md index ed55cb7..1d0e712 100644 --- a/docs/02-fase-core-business/MGN-009-reports/_MAP.md +++ b/docs/02-fase-core-business/MGN-009-reports/_MAP.md @@ -4,7 +4,7 @@ **Nombre:** Reportes y Dashboards **Fase:** 02 - Core Business **Story Points:** 35 SP -**Estado:** COMPLETADO (Sprint 8-11) +**Estado:** COMPLETADO (Sprint 8-13) **Ultima actualizacion:** 2026-01-07 --- @@ -221,17 +221,45 @@ Sistema de reportes y dashboards con generador de reportes, exportacion multi-fo | Sprint 9 | Frontend | Dashboard UI | 24 | | Sprint 10 | Frontend | Report Builder UI | 13 | | Sprint 11 | Frontend | Scheduled Reports UI | 11 | -| **TOTAL** | | | **62** | +| Sprint 12 | Frontend | Paginas, Rutas y Navegacion | 5 | +| Sprint 13 | Testing | Unit Tests (56 tests) | 4 | +| **TOTAL** | | | **71** | --- -## Pendientes Futuros +## PDF Export (BE-026 - COMPLETADO) -| Item | Descripcion | Prioridad | -|------|-------------|-----------| -| PDF Export | Integracion con puppeteer para PDF | P2 | -| Tests | Tests unitarios para componentes | P2 | -| Pages | Crear paginas/rutas para features | P1 | +| Componente | Descripcion | Estado | +|------------|-------------|--------| +| PdfService | Servicio Puppeteer para generacion PDF | Implementado | +| ReportTemplates | Templates HTML (tabular, financial, trialBalance) | Implementado | +| ExportService | Actualizado para usar PdfService | Implementado | +| Endpoints Export | /executions/:id/export, /trial-balance/export | Implementado | +| Health Check | /pdf/health para verificar estado | Implementado | + +--- + +## Frontend Pages (Sprint 12) + +| Pagina | Path | Descripcion | Estado | +|--------|------|-------------|--------| +| ReportsPage | /reports | Landing page con navegacion | Implementado | +| ReportBuilderPage | /reports/builder | Constructor visual de reportes | Implementado | +| ScheduledReportsPage | /reports/scheduled | Gestion de reportes programados | Implementado | + +--- + +## Unit Tests (Sprint 13) + +| Archivo | Feature | Tests | Cobertura | +|---------|---------|------:|-----------| +| CronBuilder.test.tsx | scheduled-reports | 12 | rendering, presets, advanced editor | +| RecipientManager.test.tsx | scheduled-reports | 13 | add/remove, validation, duplicates | +| FilterBuilder.test.tsx | report-builder | 10 | add/remove, operators | +| EntityExplorer.test.tsx | report-builder | 21 | states, categories, search | +| **TOTAL** | | **56** | | + +**Suite completa frontend:** 94 tests pasando --- @@ -241,6 +269,6 @@ Ver: [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml) --- -**Actualizado por:** Frontend-Agent (Claude Opus 4.5) +**Actualizado por:** Backend-Agent (Claude Opus 4.5) **Fecha:** 2026-01-07 -**Sprint:** 11 - COMPLETADO +**Tarea:** BE-026 - PDF Export COMPLETADO diff --git a/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-001.md b/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-001.md deleted file mode 100644 index 646bb74..0000000 --- a/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-001.md +++ /dev/null @@ -1,292 +0,0 @@ -# US-MGN009-001: Ejecutar Reportes Predefinidos - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN009-001 | -| **Modulo** | MGN-009 Reports | -| **Sprint** | Sprint 6 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 10 | -| **Estado** | Ready | -| **RF Relacionado** | RF-REPORT-001 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** ejecutar reportes predefinidos con parametros configurables -**Para** obtener informacion estructurada en diferentes formatos de salida - ---- - -## Descripcion - -El sistema proporciona reportes predefinidos por modulo con parametros configurables. Los usuarios pueden ver resultados en pantalla con paginacion o exportar a PDF, Excel, CSV. Los reportes grandes se procesan asincronamente. - -### Contexto - -- Reportes predefinidos por modulo -- Parametros configurables -- Exportacion multiformato -- Cache de resultados frecuentes - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar reportes disponibles - -```gherkin -Given un usuario con permiso "reports.view" -When accede a GET /api/v1/reports -Then el sistema responde con status 200 - And retorna lista de reportes agrupados por modulo - And solo muestra reportes para los que tiene permiso -``` - -### Escenario 2: Ver parametros de reporte - -```gherkin -Given un reporte "balance_general" -When consulta sus detalles -Then muestra los parametros disponibles: - | fecha_corte | date | required | - | nivel_detalle | select | options: [1,2,3,4] | - And muestra valores por defecto -``` - -### Escenario 3: Ejecutar reporte (vista) - -```gherkin -Given un usuario ejecutando reporte -When envia POST /api/v1/reports/balance_general/execute - | fecha_corte | "2025-12-05" | - | nivel | 3 | -Then el sistema responde con status 200 - And retorna datos con columnas y filas - And incluye totales si aplica - And soporta paginacion -``` - -### Escenario 4: Exportar a Excel - -```gherkin -Given un reporte ejecutado -When solicita POST /api/v1/reports/balance_general/export - | format | "xlsx" | - | parameters | {...} | -Then retorna jobId para tracking - And el proceso se ejecuta asincronamente -``` - -### Escenario 5: Consultar estado de exportacion - -```gherkin -Given una exportacion en proceso -When consulta GET /api/v1/reports/executions/{id} -Then retorna status y progreso - And cuando completa incluye downloadUrl - And indica expiresAt para el archivo -``` - -### Escenario 6: Exportar a PDF - -```gherkin -Given un reporte con < 500 filas -When exporta a PDF -Then genera archivo PDF formateado - And incluye header con logo y titulo - And incluye fecha de generacion -``` - -### Escenario 7: Ordenar resultados - -```gherkin -Given un reporte con multiples columnas -When especifica sort = { field: "total", order: "desc" } -Then los resultados se ordenan por total descendente -``` - -### Escenario 8: Filtrar por modulo - -```gherkin -Given reportes de multiples modulos -When consulta GET /api/v1/reports?module=financial -Then retorna solo reportes del modulo financiero -``` - -### Escenario 9: Cache de resultados - -```gherkin -Given un reporte ejecutado recientemente con mismos parametros -When otro usuario ejecuta el mismo reporte -Then los resultados se sirven desde cache - And el tiempo de respuesta es < 100ms -``` - -### Escenario 10: Reporte con datos vacios - -```gherkin -Given parametros que no retornan datos -When ejecuta el reporte -Then retorna array vacio con mensaje informativo - And la exportacion no genera error -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| REPORTES | -+------------------------------------------------------------------+ -| [Modulo: Todos v] [Categoria: Todas v] [Buscar...] | -+------------------------------------------------------------------+ - -CONTABILIDAD -+------------------------------------------------------------------+ -| Balance General [Ver] [Ejecutar] | -| Estado de situacion financiera a una fecha | -| | -| Estado de Resultados [Ver] [Ejecutar] | -| Ingresos y gastos del periodo | -| | -| Libro Mayor [Ver] [Ejecutar] | -| Movimientos por cuenta contable | -+------------------------------------------------------------------+ - -VENTAS -+------------------------------------------------------------------+ -| Ventas por Cliente [Ver] [Ejecutar] | -| Resumen de ventas agrupado por cliente | -+------------------------------------------------------------------+ - -Modal: Ejecutar Reporte -+------------------------------------------------------------------+ -| EJECUTAR REPORTE: Balance General [X] | -+------------------------------------------------------------------+ -| Descripcion: Estado de situacion financiera a una fecha | -| | -| PARAMETROS | -+------------------------------------------------------------------+ -| Fecha de Corte* | [2025-12-05 ] [📅] | -| Nivel de Detalle | [3 - Subcuentas v ] | -| Incluir cuentas en cero | [ ] | -+------------------------------------------------------------------+ -| | -| [Cancelar] [Excel v] [PDF] [=== Ejecutar ===] | -+------------------------------------------------------------------+ - -Resultados: -+------------------------------------------------------------------+ -| BALANCE GENERAL Al 05 de Diciembre 2025 | -+------------------------------------------------------------------+ -| [Exportar: Excel v] [PDF] [CSV] Pagina 1 de 5 | -+------------------------------------------------------------------+ - -| CUENTA | NOMBRE | SALDO DEUDOR | SALDO ACREE | -+------------------------------------------------------------------+ -| 1 | ACTIVO | | | -| 1.1 | Activo Circulante | | | -| 1.1.1 | Caja | 50,000 | | -| 1.1.2 | Bancos | 200,000 | | -| 1.2 | Activo Fijo | | | -| 1.2.1 | Mobiliario | 150,000 | | -+------------------------------------------------------------------+ -| TOTAL ACTIVO | 400,000 | | -+------------------------------------------------------------------+ -| 2 | PASIVO | | | -| 2.1 | Pasivo Corto Plazo | | | -| 2.1.1 | Proveedores | | 100,000 | -+------------------------------------------------------------------+ -| TOTAL PASIVO | | 100,000 | -+------------------------------------------------------------------+ -| 3 | CAPITAL | | | -| 3.1 | Capital Social | | 250,000 | -| 3.2 | Resultado Ejerc. | | 50,000 | -+------------------------------------------------------------------+ -| TOTAL CAPITAL | | 300,000 | -+------------------------------------------------------------------+ - [< Anterior] [1] [2] [Siguiente >] -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /reports | Listar disponibles | -| GET | /reports/:code | Detalle de reporte | -| POST | /reports/:code/execute | Ejecutar (vista) | -| POST | /reports/:code/export | Exportar | -| GET | /reports/executions/:id | Estado exportacion | - -### Tipos de Parametros - -| Tipo | UI Component | Ejemplo | -|------|--------------|---------| -| date | DatePicker | fecha_corte | -| date_range | RangePicker | periodo | -| select | Dropdown | nivel_detalle | -| multiselect | MultiSelect | cuentas | -| entity | EntitySearch | cliente_id | -| boolean | Checkbox | incluir_ceros | - -### Cache - -```typescript -// Cache key basado en reporte + params + tenant -const cacheKey = `report:${code}:${tenantId}:${hash(params)}`; -const TTL = 5 * 60; // 5 minutos -``` - ---- - -## Definicion de Done - -- [ ] Catalogo de reportes predefinidos -- [ ] UI de parametros dinamica -- [ ] Ejecucion con paginacion -- [ ] Exportacion PDF, Excel, CSV -- [ ] Jobs asincronos para exportacion -- [ ] Cache de resultados -- [ ] Permisos por reporte -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla report_definitions | Definiciones | -| Tabla report_executions | Historial | -| Bull Queue | Para exportacion | -| ExcelJS | Para XLSX | -| Puppeteer | Para PDF | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN009-003 | Report Builder | -| US-MGN009-004 | Reportes Programados | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-002.md b/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-002.md deleted file mode 100644 index 0598a1d..0000000 --- a/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-002.md +++ /dev/null @@ -1,284 +0,0 @@ -# US-MGN009-002: Gestionar Dashboards y Widgets - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN009-002 | -| **Modulo** | MGN-009 Reports | -| **Sprint** | Sprint 6 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 13 | -| **Estado** | Ready | -| **RF Relacionado** | RF-REPORT-002 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema -**Quiero** crear y personalizar dashboards con widgets de KPIs y graficos -**Para** visualizar metricas clave de mi negocio en tiempo real - ---- - -## Descripcion - -Los usuarios crean dashboards personalizados con widgets configurables. Los widgets muestran KPIs, graficos, tablas y otros visuales. Soporta drag-and-drop, refresh automatico y compartir entre usuarios. - -### Contexto - -- Dashboards personalizables -- Widgets de diferentes tipos -- Refresh en tiempo real -- Compartir con equipo - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver dashboard por defecto - -```gherkin -Given un usuario nuevo -When accede al dashboard -Then ve el dashboard por defecto del tenant - And los widgets se cargan con datos actuales -``` - -### Escenario 2: Listar mis dashboards - -```gherkin -Given un usuario con dashboards creados -When consulta GET /api/v1/dashboards -Then retorna sus dashboards + los compartidos - And indica cual es el default -``` - -### Escenario 3: Crear nuevo dashboard - -```gherkin -Given un usuario con permiso -When crea POST /api/v1/dashboards - | name | "Mi Dashboard de Ventas" | -Then el dashboard se crea vacio - And puede agregar widgets -``` - -### Escenario 4: Agregar widget KPI - -```gherkin -Given un dashboard existente -When agrega widget tipo KPI - | title | "Ventas Hoy" | - | dataSource | query: "sales_today" | - | format | "currency" | -Then el widget aparece en el dashboard - And muestra el valor actual y tendencia -``` - -### Escenario 5: Agregar widget grafico - -```gherkin -Given un dashboard existente -When agrega widget tipo line_chart - | title | "Ventas Mensuales" | - | dataSource | query: "sales_by_month" | -Then el widget muestra grafico de lineas - And es interactivo (hover, zoom) -``` - -### Escenario 6: Reorganizar widgets (drag & drop) - -```gherkin -Given un dashboard con multiples widgets -When el usuario arrastra un widget a nueva posicion -Then el layout se actualiza - And se guarda automaticamente -``` - -### Escenario 7: Redimensionar widget - -```gherkin -Given un widget en el dashboard -When el usuario cambia su tamano -Then el widget se redimensiona - And el contenido se adapta -``` - -### Escenario 8: Refresh automatico - -```gherkin -Given dashboard con refreshInterval = 60 -When pasan 60 segundos -Then todos los widgets se actualizan - And se muestra indicador de actualizacion -``` - -### Escenario 9: Compartir dashboard - -```gherkin -Given un dashboard personal -When lo marca como compartido (isShared = true) -Then otros usuarios del tenant pueden verlo - And aparece en su lista de dashboards -``` - -### Escenario 10: Configurar widget - -```gherkin -Given un widget existente -When abre su configuracion -Then puede cambiar: - | title, colores, formato, fuente de datos | - And los cambios se reflejan inmediatamente -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| DASHBOARD: Executive Overview [+ Widget] [⚙] | -+------------------------------------------------------------------+ -| [Mis Dashboards v] [Periodo: Hoy v] Ultima actualizacion: hace 30s | -+------------------------------------------------------------------+ - -+----------------+ +----------------+ +----------------+ -| 💰 VENTAS HOY | | 📦 PEDIDOS | | 👥 USUARIOS | -| $125,000 | | 45 | | 1,234 | -| ↑ 15% | | ↑ 8% | | ↓ 2% | -+----------------+ +----------------+ +----------------+ - -+-----------------------------------+ +-----------------------------------+ -| VENTAS MENSUALES | | DISTRIBUCION POR REGION | -| | | | -| ^ | | Norte | -| $1M| ___ | | ████████ 45% | -| | / \ | | Centro | -| $500|___/ \___ | | ██████ 35% | -| | \ | | Sur | -| $0+---------------+ | | ████ 20% | -| E F M A M J J | | | -+-----------------------------------+ +-----------------------------------+ - -+------------------------------------------------------------------+ -| ULTIMOS PEDIDOS | -+------------------------------------------------------------------+ -| #1234 | Juan Perez | $1,500 | Pendiente | hace 5 min | -| #1233 | Maria Garcia | $2,300 | Enviado | hace 15 min | -| #1232 | Carlos Lopez | $800 | Completado | hace 30 min | -+------------------------------------------------------------------+ - -Modal: Agregar Widget -+------------------------------------------------------------------+ -| AGREGAR WIDGET [X] | -+------------------------------------------------------------------+ -| TIPO DE WIDGET | -| [KPI] [Grafico Lineas] [Grafico Barras] [Pastel] [Tabla] [Texto] | -+------------------------------------------------------------------+ -| | -| Titulo* | -| [Ventas por Categoria ] | -| | -| Fuente de Datos | -| [Query: sales_by_category v] | -| | -| Configuracion | -| [✓] Mostrar leyenda | -| [✓] Mostrar valores | -| Colores: [Paleta 1 v] | -| | -| Tamano | -| Ancho: [6 columnas v] Alto: [4 filas v] | -| | -| [Cancelar] [=== Agregar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /dashboards | Listar dashboards | -| GET | /dashboards/:id | Dashboard con widgets | -| POST | /dashboards | Crear dashboard | -| PUT | /dashboards/:id | Actualizar | -| DELETE | /dashboards/:id | Eliminar | -| POST | /dashboards/:id/widgets | Agregar widget | -| PUT | /dashboards/:id/widgets/:wid | Actualizar widget | -| DELETE | /dashboards/:id/widgets/:wid | Eliminar widget | -| PUT | /dashboards/:id/layout | Actualizar layout | -| GET | /dashboards/:id/widgets/:wid/data | Datos de widget | - -### Tipos de Widget - -| Tipo | Libreria | Uso | -|------|----------|-----| -| KPI | Custom | Numero con tendencia | -| line_chart | Chart.js | Tendencias | -| bar_chart | Chart.js | Comparativas | -| pie_chart | Chart.js | Distribucion | -| table | Custom | Datos tabulares | -| gauge | Custom | Indicador circular | - -### Layout Grid - -```typescript -// Grid de 12 columnas -interface WidgetPosition { - widgetId: UUID; - x: number; // 0-11 (columna) - y: number; // fila - w: number; // 1-12 (ancho) - h: number; // altura en filas -} -``` - ---- - -## Definicion de Done - -- [ ] CRUD dashboards -- [ ] 10+ tipos de widgets -- [ ] Drag & drop de widgets -- [ ] Redimensionamiento -- [ ] Refresh automatico -- [ ] Compartir dashboards -- [ ] Datos en tiempo real -- [ ] Responsive design -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla dashboards | Storage | -| Tabla widgets | Widgets | -| Chart.js | Graficos | -| react-grid-layout | Layout | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Home page | Dashboard por defecto | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-003.md b/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-003.md deleted file mode 100644 index b91093e..0000000 --- a/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-003.md +++ /dev/null @@ -1,304 +0,0 @@ -# US-MGN009-003: Crear Reportes Personalizados con Builder - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN009-003 | -| **Modulo** | MGN-009 Reports | -| **Sprint** | Sprint 6 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 8 | -| **Estado** | Ready | -| **RF Relacionado** | RF-REPORT-003 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario avanzado -**Quiero** crear reportes personalizados sin escribir SQL -**Para** obtener informacion especifica que no esta en reportes predefinidos - ---- - -## Descripcion - -El Report Builder permite a usuarios avanzados crear reportes personalizados seleccionando tablas, campos, filtros y agregaciones visualmente. El sistema genera el query automaticamente respetando permisos y RLS. - -### Contexto - -- Interfaz visual drag & drop -- Modelo de datos expuesto -- Joins automaticos -- Respeta permisos y RLS - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver modelo de datos disponible - -```gherkin -Given un usuario con permiso "reports.builder" -When accede a GET /api/v1/reports/builder/model -Then retorna entidades disponibles con sus campos - And solo muestra entidades permitidas por rol - And incluye relaciones entre entidades -``` - -### Escenario 2: Seleccionar entidad principal - -```gherkin -Given el report builder abierto -When selecciona entidad "contacts" -Then se agrega al canvas - And muestra lista de campos disponibles - And muestra relaciones disponibles -``` - -### Escenario 3: Agregar campos al reporte - -```gherkin -Given entidad "contacts" seleccionada -When arrastra campos "name", "email", "total_sales" -Then los campos se agregan a la seleccion - And aparecen en la preview -``` - -### Escenario 4: Agregar entidad relacionada - -```gherkin -Given entidad "contacts" en el canvas -When agrega entidad relacionada "orders" -Then el JOIN se configura automaticamente - And puede seleccionar campos de ambas entidades -``` - -### Escenario 5: Aplicar agregacion - -```gherkin -Given campo "orders.total" seleccionado -When configura aggregate = "sum" - And groupBy = ["contacts.name"] -Then el reporte agrupa por contacto - And muestra suma de pedidos -``` - -### Escenario 6: Agregar filtro - -```gherkin -Given campos seleccionados -When agrega filtro: - | field | "orders.created_at" | - | operator | "between" | - | value | ["2025-01-01", "2025-12-31"] | -Then el filtro se aplica a los resultados -``` - -### Escenario 7: Crear parametro dinamico - -```gherkin -Given un filtro agregado -When lo marca como parametro - | parameterName | "fecha_desde" | - | label | "Desde" | -Then el filtro se convierte en parametro - And se pedira al ejecutar el reporte -``` - -### Escenario 8: Preview de resultados - -```gherkin -Given reporte configurado -When hace click en "Preview" -Then muestra primeras 100 filas - And muestra tiempo de ejecucion - And muestra advertencias si aplica -``` - -### Escenario 9: Validar reporte - -```gherkin -Given configuracion con errores - | campos sin entidad | - | filtro incompleto | -When valida el reporte -Then muestra lista de errores - And no permite guardar hasta corregir -``` - -### Escenario 10: Guardar y compartir reporte - -```gherkin -Given reporte valido -When guarda con: - | name | "Ventas por Cliente 2025" | - | isPublic | true | -Then el reporte se guarda - And aparece en lista de reportes del tenant - And puede ejecutarse como reporte normal -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| REPORT BUILDER | -+------------------------------------------------------------------+ -| [Nuevo] [Abrir v] [Guardar] [Preview] [❌] | -+------------------------------------------------------------------+ - -+------------------------+----------------------------------------+ -| ENTIDADES | CANVAS | -+------------------------+ | -| 🔍 Buscar... | +---------------+ +---------------+ | -| | | CONTACTS |--->| ORDERS | | -| 📁 Catalogos | +---------------+ +---------------+ | -| 📋 Contacts | | -| 📋 Products | | -| 📋 Categories | | -| | | -| 📁 Ventas | CAMPOS SELECCIONADOS | -| 📋 Orders | +------------------------------------+ | -| 📋 Order Items | | contacts.name | Nombre Cliente | | -| | | contacts.email | Email | | -| 📁 Financiero | | orders.total | Total (SUM) | | -| 📋 Accounts | +------------------------------------+ | -| 📋 Journal Entries | | -+------------------------+-----------------------------------------+ - -+------------------------------------------------------------------+ -| FILTROS | -+------------------------------------------------------------------+ -| orders.created_at | >= | [2025-01-01] | [ ] Parametro | -| [+ Agregar Filtro] | -+------------------------------------------------------------------+ - -+------------------------------------------------------------------+ -| AGRUPACION Y ORDEN | -+------------------------------------------------------------------+ -| Agrupar por: [contacts.name v] | -| Ordenar por: [Total ▼] | -+------------------------------------------------------------------+ - -+------------------------------------------------------------------+ -| PREVIEW 100 filas | -+------------------------------------------------------------------+ -| NOMBRE CLIENTE | EMAIL | TOTAL | -+------------------------------------------------------------------+ -| Juan Perez | juan@mail.com | $45,000 | -| Maria Garcia | maria@mail.com | $38,000 | -| Carlos Lopez | carlos@mail.com | $22,000 | -+------------------------------------------------------------------+ - -Validacion: -+------------------------------------------------------------------+ -| ⚠️ ADVERTENCIAS | -+------------------------------------------------------------------+ -| - La consulta puede retornar mas de 10,000 filas | -| - Considere agregar filtro de fecha | -+------------------------------------------------------------------+ - -Modal: Guardar Reporte -+------------------------------------------------------------------+ -| GUARDAR REPORTE [X] | -+------------------------------------------------------------------+ -| Nombre* | -| [Ventas por Cliente 2025 ] | -| | -| Descripcion | -| [Reporte de ventas agrupado por cliente para el año 2025 ] | -| | -| [✓] Compartir con el equipo | -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /reports/builder/model | Modelo de datos | -| POST | /reports/builder/validate | Validar config | -| POST | /reports/builder/preview | Preview | -| POST | /reports/custom | Guardar reporte | -| GET | /reports/custom | Listar mis reportes | -| PUT | /reports/custom/:id | Actualizar | -| DELETE | /reports/custom/:id | Eliminar | - -### Operadores de Filtro - -| Operador | SQL | Tipos | -|----------|-----|-------| -| eq | = | todos | -| ne | <> | todos | -| gt, gte | >, >= | number, date | -| lt, lte | <, <= | number, date | -| like | LIKE | string | -| in | IN | todos | -| between | BETWEEN | number, date | -| is_null | IS NULL | todos | - -### Generacion de Query - -```typescript -// El sistema genera query parametrizado -// Siempre incluye tenant_id filter (RLS) -const query = buildQuery({ - entities: ['contacts', 'orders'], - fields: [...], - filters: [...], - groupBy: [...], - tenantId: context.tenantId // Automatico -}); -``` - ---- - -## Definicion de Done - -- [ ] Modelo de datos expuesto -- [ ] UI de entity explorer -- [ ] Drag & drop de campos -- [ ] Constructor de filtros -- [ ] Agregaciones (SUM, AVG, COUNT) -- [ ] JOINs automaticos -- [ ] Preview en tiempo real -- [ ] Guardar y compartir -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN009-001 | Infraestructura de reportes | -| Tabla custom_reports | Storage | -| Tabla report_fields | Campos | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Reportes avanzados | Usuarios crean propios | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-004.md b/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-004.md deleted file mode 100644 index d8361c8..0000000 --- a/docs/02-fase-core-business/MGN-009-reports/historias-usuario/US-MGN009-004.md +++ /dev/null @@ -1,310 +0,0 @@ -# US-MGN009-004: Programar Ejecucion Automatica de Reportes - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN009-004 | -| **Modulo** | MGN-009 Reports | -| **Sprint** | Sprint 6 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 5 | -| **Estado** | Ready | -| **RF Relacionado** | RF-REPORT-004 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador -**Quiero** programar ejecucion automatica de reportes -**Para** recibir informacion periodica sin tener que ejecutar manualmente - ---- - -## Descripcion - -Los reportes se pueden programar para ejecutarse automaticamente (diario, semanal, mensual) y enviarse por email a destinatarios configurados. Soporta parametros dinamicos con fechas relativas. - -### Contexto - -- Programacion con expresiones cron -- Envio automatico por email -- Parametros con fechas relativas -- Historial de ejecuciones - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar programaciones - -```gherkin -Given un usuario con reportes programados -When accede a GET /api/v1/reports/schedules -Then retorna lista de programaciones - And muestra proxima ejecucion - And muestra ultima ejecucion -``` - -### Escenario 2: Crear programacion diaria - -```gherkin -Given un reporte existente -When crea programacion: - | schedule.type | "cron" | - | schedule.cron | "0 8 * * *" | - | timezone | "America/Mexico_City" | -Then se calcula nextRunAt = manana 8:00 AM - And la programacion queda activa -``` - -### Escenario 3: Programacion semanal - -```gherkin -Given necesidad de reporte semanal -When configura: - | schedule.cron | "0 8 * * 1" | -Then el reporte se ejecuta cada lunes a las 8:00 -``` - -### Escenario 4: Parametros con fechas relativas - -```gherkin -Given reporte con parametro fecha_desde -When configura: - | fecha_desde | { type: "relative_date", value: "start_of_week" } | -Then al ejecutar, fecha_desde = inicio de la semana actual -``` - -### Escenario 5: Configurar destinatarios - -```gherkin -Given programacion creada -When agrega destinatarios: - | type: "user", value: "uuid-user" | - | type: "email", value: "externo@mail.com" | - | type: "role", value: "sales_manager" | -Then al ejecutar se envia a todos los destinatarios -``` - -### Escenario 6: Ejecucion automatica - -```gherkin -Given programacion activa con nextRunAt = ahora -When el cron job se ejecuta -Then el reporte se genera - And se envia por email con adjunto - And se actualiza lastRunAt y nextRunAt -``` - -### Escenario 7: Skip si sin datos - -```gherkin -Given programacion con skipIfEmpty = true - And el reporte no tiene datos para el periodo -When se ejecuta -Then NO se envia email - And se registra como "skipped" -``` - -### Escenario 8: Ejecutar manualmente - -```gherkin -Given programacion existente -When envia POST /api/v1/reports/schedules/{id}/run -Then el reporte se ejecuta inmediatamente - And no afecta la proxima ejecucion programada -``` - -### Escenario 9: Pausar programacion - -```gherkin -Given programacion activa -When cambia isActive = false -Then la programacion se pausa - And no se ejecuta hasta reactivar -``` - -### Escenario 10: Ver historial de ejecuciones - -```gherkin -Given programacion con ejecuciones previas -When consulta GET /api/v1/reports/schedules/{id}/history -Then retorna lista de ejecuciones: - | startedAt, status, duration, rowCount, recipientsSent | -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| REPORTES PROGRAMADOS [+ Nueva Programacion]| -+------------------------------------------------------------------+ -| [Estado: Todos v] [Buscar...] | -+------------------------------------------------------------------+ - -| NOMBRE | REPORTE | FRECUENCIA | PROXIMA | ESTADO | -+------------------------------------------------------------------+ -| Ventas Semanales | Ventas x Cliente| Lunes 8:00 | 09/12 | ✓ Activo | -| Balance Mensual | Balance General | Dia 1 8:00 | 01/01 | ✓ Activo | -| Inventario Diario | Stock Bajo | Diario 7:00| 06/12 | ⏸ Pausado| -+------------------------------------------------------------------+ - -Detalle de Programacion: -+------------------------------------------------------------------+ -| VENTAS SEMANALES [⚙] [▶]| -+------------------------------------------------------------------+ -| Reporte: Ventas por Cliente | -| Formato: Excel | -| Programacion: Cada lunes a las 8:00 AM (America/Mexico_City) | -| Proxima ejecucion: Lunes 9 de Diciembre 2025, 8:00 AM | -| | -| PARAMETROS | -| - fecha_desde: Inicio de la semana | -| - fecha_hasta: Fin de la semana | -| | -| DESTINATARIOS | -| - Juan Perez (juan@empresa.com) | -| - Maria Garcia (maria@empresa.com) | -| - Rol: Gerentes de Ventas (3 usuarios) | -| | -| OPCIONES | -| [✓] No enviar si no hay datos | -| [✓] Adjuntar archivo | -| [✓] Incluir link al reporte | -+------------------------------------------------------------------+ - -+------------------------------------------------------------------+ -| HISTORIAL DE EJECUCIONES | -+------------------------------------------------------------------+ -| FECHA | ESTADO | DURACION | FILAS | ENVIADOS | -+------------------------------------------------------------------+ -| 2025-12-02 08:00 | ✓ Exito | 15s | 150 | 5 | -| 2025-11-25 08:00 | ✓ Exito | 12s | 142 | 5 | -| 2025-11-18 08:00 | ⏭ Skip | - | 0 | 0 | -| 2025-11-11 08:00 | ✓ Exito | 18s | 165 | 5 | -+------------------------------------------------------------------+ - -Modal: Nueva Programacion -+------------------------------------------------------------------+ -| PROGRAMAR REPORTE [X] | -+------------------------------------------------------------------+ -| Nombre* | -| [Ventas Semanales para Gerencia ] | -| | -| Reporte* | -| [Ventas por Cliente v] | -| | -| PROGRAMACION | -+------------------------------------------------------------------+ -| Frecuencia: ( ) Una vez ( ) Diario (•) Semanal ( ) Mensual | -| Dia: [Lunes v] Hora: [08:00 v] | -| Zona Horaria: [America/Mexico_City v] | -+------------------------------------------------------------------+ -| | -| PARAMETROS | -+------------------------------------------------------------------+ -| fecha_desde: [Inicio de semana v] | -| fecha_hasta: [Fin de semana v] | -+------------------------------------------------------------------+ -| | -| DESTINATARIOS | -+------------------------------------------------------------------+ -| [+ Usuario] [+ Email] [+ Rol] | -| 👤 Juan Perez [x] | -| 📧 gerencia@empresa.com [x] | -+------------------------------------------------------------------+ -| | -| FORMATO Y OPCIONES | -+------------------------------------------------------------------+ -| Formato: (•) Excel ( ) PDF ( ) CSV | -| [✓] No enviar si sin datos | -| [✓] Adjuntar archivo (max 10MB) | -| | -| [Cancelar] [=== Programar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /reports/schedules | Listar programaciones | -| POST | /reports/schedules | Crear programacion | -| GET | /reports/schedules/:id | Detalle | -| PUT | /reports/schedules/:id | Actualizar | -| DELETE | /reports/schedules/:id | Eliminar | -| POST | /reports/schedules/:id/run | Ejecutar ahora | -| PUT | /reports/schedules/:id/status | Pausar/Activar | -| GET | /reports/schedules/:id/history | Historial | - -### Expresiones Cron - -| Expresion | Descripcion | -|-----------|-------------| -| 0 8 * * * | Diario 8:00 | -| 0 8 * * 1 | Lunes 8:00 | -| 0 8 1 * * | Dia 1 del mes 8:00 | -| 0 8 * * 1-5 | Lun-Vie 8:00 | - -### Fechas Relativas - -| Valor | Significado | -|-------|-------------| -| today | Hoy | -| yesterday | Ayer | -| start_of_week | Inicio de semana | -| end_of_week | Fin de semana | -| start_of_month | Inicio de mes | -| -7d | Hace 7 dias | -| -1m | Hace 1 mes | - ---- - -## Definicion de Done - -- [ ] CRUD programaciones -- [ ] Parser de expresiones cron -- [ ] Parametros relativos -- [ ] Job scheduler -- [ ] Envio por email -- [ ] Historial de ejecuciones -- [ ] Pausar/Reanudar -- [ ] Ejecucion manual -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN009-001 | Reportes predefinidos | -| US-MGN008-002 | Envio de emails | -| Bull Queue | Scheduler | -| node-cron | Parser cron | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Automatizacion | Reportes automaticos | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-009-reports/implementacion/TRACEABILITY.yml b/docs/02-fase-core-business/MGN-009-reports/implementacion/TRACEABILITY.yml index c38fb1d..3c32700 100644 --- a/docs/02-fase-core-business/MGN-009-reports/implementacion/TRACEABILITY.yml +++ b/docs/02-fase-core-business/MGN-009-reports/implementacion/TRACEABILITY.yml @@ -7,7 +7,7 @@ epic_name: Reports phase: 2 phase_name: Core Business story_points: 35 -status: completed # Sprint 8-11: Backend + Frontend completo +status: completed # Sprint 8-13: Backend + Frontend + Tests completo last_updated: "2026-01-07" # ============================================================================= @@ -253,12 +253,37 @@ implementation: - name: ExportService file: apps/backend/src/modules/reports/export.service.ts - status: pending + status: completed requirement: RF-REPORT-001 methods: - - {name: toPdf, description: Exportar a PDF} - - {name: toExcel, description: Exportar a Excel} - - {name: toCsv, description: Exportar a CSV} + - {name: export, description: Exportar datos a formato especificado} + - {name: exportToPdf, description: Exportar a PDF usando PdfService} + - {name: exportToXlsx, description: Exportar a Excel} + - {name: exportToCsv, description: Exportar a CSV} + - {name: exportToJson, description: Exportar a JSON} + - {name: exportToHtml, description: Exportar a HTML} + + - name: PdfService + file: apps/backend/src/modules/reports/pdf.service.ts + status: completed + requirement: RF-REPORT-001 + methods: + - {name: generateFromHtml, description: Generar PDF desde HTML} + - {name: generateFromUrl, description: Generar PDF desde URL} + - {name: healthCheck, description: Verificar disponibilidad del servicio} + - {name: close, description: Cerrar instancia del navegador} + dependencies: + - {package: puppeteer, version: "^22.15.0", purpose: "Headless browser for PDF generation"} + + - name: ReportTemplates + file: apps/backend/src/modules/reports/templates/report-templates.ts + status: completed + requirement: RF-REPORT-001 + methods: + - {name: generateTabularReport, description: Generar reporte tabular HTML} + - {name: generateFinancialReport, description: Generar reporte financiero HTML} + - {name: generateTrialBalance, description: Generar balanza de comprobacion HTML} + - {name: generateDashboardExport, description: Generar exportacion de dashboard HTML} - name: DashboardsService file: apps/backend/src/modules/reports/dashboards.service.ts @@ -310,9 +335,19 @@ implementation: description: Ejecutar reporte requirement: RF-REPORT-001 + - method: POST + path: /api/v1/reports/executions/:id/export + description: Exportar resultado de ejecucion + requirement: RF-REPORT-001 + - method: GET - path: /api/v1/reports/:id/export/:format - description: Exportar reporte + path: /api/v1/reports/quick/trial-balance/export + description: Exportar balanza de comprobacion + requirement: RF-REPORT-001 + + - method: GET + path: /api/v1/reports/pdf/health + description: Verificar estado servicio PDF requirement: RF-REPORT-001 - method: GET @@ -430,6 +465,74 @@ implementation: - {package: "react-grid-layout", version: "^1.4.4", purpose: "Grid drag & drop"} - {package: "recharts", version: "^2.10.x", purpose: "Charts"} + pages: + path: frontend/src/pages/reports/ + status: completed + + files: + - name: ReportsPage + file: ReportsPage.tsx + status: completed + description: "Landing page con cards de navegacion" + + - name: ReportBuilderPage + file: ReportBuilderPage.tsx + status: completed + description: "Pagina que integra ReportBuilder component" + + - name: ScheduledReportsPage + file: ScheduledReportsPage.tsx + status: completed + description: "Pagina CRUD para reportes programados" + + tests: + path: frontend/src/features/ + framework: Vitest + Testing Library + status: completed + + files: + - name: CronBuilder.test.tsx + file: scheduled-reports/__tests__/CronBuilder.test.tsx + tests: 12 + status: completed + coverage: + - rendering + - presets_dropdown + - advanced_editor + - cron_descriptions + + - name: RecipientManager.test.tsx + file: scheduled-reports/__tests__/RecipientManager.test.tsx + tests: 13 + status: completed + coverage: + - rendering + - adding_recipients + - validation + - duplicate_detection + - removing_recipients + + - name: FilterBuilder.test.tsx + file: report-builder/__tests__/FilterBuilder.test.tsx + tests: 10 + status: completed + coverage: + - rendering + - adding_filters + - removing_filters + - operator_types + + - name: EntityExplorer.test.tsx + file: report-builder/__tests__/EntityExplorer.test.tsx + tests: 21 + status: completed + coverage: + - loading_state + - error_state + - category_expansion + - entity_selection + - search_filtering + # ============================================================================= # DEPENDENCIAS # ============================================================================= @@ -458,7 +561,7 @@ dependencies: metrics: story_points: estimated: 35 - actual: 35 # Sprint 8: 10, Sprint 9: 10, Sprint 10: 8, Sprint 11: 7 + actual: 45 # Sprint 8: 10, Sprint 9: 10, Sprint 10: 8, Sprint 11: 7, Sprint 12: 5, Sprint 13: 5 documentation: requirements: 4 @@ -467,9 +570,10 @@ metrics: files: database: 1 # 14-reports.sql (12 tablas) - backend: 14 - frontend: 48 # Sprint 9 (24) + Sprint 10 (13) + Sprint 11 (11) - total: 63 + backend: 17 # +3: pdf.service.ts, templates/report-templates.ts, export.service.ts updated + frontend: 56 # Sprint 9 (24) + Sprint 10 (13) + Sprint 11 (11) + Sprint 12 (4) + Sprint 13 (4) + tests: 4 # Sprint 13 + total: 78 sprints: - sprint: 8 @@ -491,6 +595,16 @@ metrics: status: completed date: "2026-01-07" feature: scheduled-reports + - sprint: 12 + layer: frontend + status: completed + date: "2026-01-07" + feature: pages-navigation + - sprint: 13 + layer: testing + status: completed + date: "2026-01-07" + feature: unit-tests # ============================================================================= # HISTORIAL @@ -572,4 +686,40 @@ history: - "4 formatos de exportacion: PDF, Excel, CSV, JSON" - "TypeScript build validado" - "Vite build validado" - - "Modulo MGN-009 completado al 100%" + + - date: "2026-01-07" + action: "Sprint 12 - Pages & Navigation" + author: Frontend-Agent + changes: + - "ReportsPage - Landing page con cards de navegacion" + - "ReportBuilderPage - Integra componente ReportBuilder" + - "ScheduledReportsPage - Integra ScheduleList, ScheduleForm, ExecutionHistory" + - "routes.tsx - Rutas lazy loading para /reports/*" + - "DashboardLayout.tsx - Items Dashboards y Reportes en sidebar" + - "TypeScript build validado" + - "Vite build validado" + + - date: "2026-01-07" + action: "Sprint 13 - Unit Tests" + author: Frontend-Agent + changes: + - "CronBuilder.test.tsx - 12 tests para constructor cron" + - "RecipientManager.test.tsx - 13 tests para gestion destinatarios" + - "FilterBuilder.test.tsx - 10 tests para constructor filtros" + - "EntityExplorer.test.tsx - 21 tests para explorador entidades" + - "Total: 56 tests nuevos, 94 tests en suite completa" + - "Cobertura: rendering, interaccion, validacion, estados" + - "Modulo MGN-009 completado al 100% con tests" + + - date: "2026-01-07" + action: "BE-026 - PDF Export Implementation" + author: Backend-Agent + changes: + - "PdfService - Servicio puppeteer para generacion PDF" + - "ReportTemplates - Templates HTML para reportes financieros" + - "ExportService actualizado para usar PdfService" + - "Endpoints exportacion: /executions/:id/export, /trial-balance/export" + - "Endpoint health check: /pdf/health" + - "Soporte para formatos: PDF, XLSX, CSV, JSON, HTML" + - "Templates: tabular, financial, trialBalance, dashboard" + - "TypeScript build validado" diff --git a/docs/02-fase-core-business/MGN-010-financial/especificaciones/ET-FIN-backend.md b/docs/02-fase-core-business/MGN-010-financial/especificaciones/ET-FIN-backend.md index 9fec5b6..c2978d9 100644 --- a/docs/02-fase-core-business/MGN-010-financial/especificaciones/ET-FIN-backend.md +++ b/docs/02-fase-core-business/MGN-010-financial/especificaciones/ET-FIN-backend.md @@ -6,11 +6,11 @@ |-------|-------| | **ID** | ET-FIN-BACKEND | | **Modulo** | MGN-010 Financial | -| **Version** | 1.0 | +| **Version** | 2.0 | | **Estado** | En Diseno | | **Framework** | NestJS | | **Autor** | Requirements-Analyst | -| **Fecha** | 2025-12-05 | +| **Fecha** | 2026-01-10 | --- @@ -35,7 +35,15 @@ apps/backend/src/modules/financial/ │ ├── fiscal-years.service.ts │ ├── fiscal-periods.service.ts │ ├── journal.service.ts -│ └── cost-centers.service.ts +│ ├── cost-centers.service.ts +│ ├── invoices.service.ts +│ ├── payments.service.ts +│ ├── payment-methods.service.ts +│ ├── payment-terms.service.ts +│ ├── journal-entries.service.ts +│ ├── reconcile-models.service.ts +│ ├── taxes.service.ts +│ └── incoterms.service.ts ├── entities/ │ ├── account-type.entity.ts │ ├── chart-of-accounts.entity.ts @@ -45,8 +53,20 @@ apps/backend/src/modules/financial/ │ ├── fiscal-year.entity.ts │ ├── fiscal-period.entity.ts │ ├── journal-entry.entity.ts +│ ├── journal-entry-line.entity.ts │ ├── journal-line.entity.ts -│ └── cost-center.entity.ts +│ ├── cost-center.entity.ts +│ ├── invoice.entity.ts +│ ├── invoice-line.entity.ts +│ ├── payment.entity.ts +│ ├── payment-invoice.entity.ts +│ ├── payment-method.entity.ts +│ ├── payment-term.entity.ts +│ ├── payment-term-line.entity.ts +│ ├── reconcile-model.entity.ts +│ ├── reconcile-model-line.entity.ts +│ ├── tax.entity.ts +│ └── incoterm.entity.ts ├── dto/ │ ├── create-account.dto.ts │ ├── create-journal-entry.dto.ts @@ -266,6 +286,646 @@ export class JournalLine { } ``` +### Invoice Entity + +```typescript +export enum InvoiceType { + CUSTOMER = 'customer', + SUPPLIER = 'supplier', +} + +export enum InvoiceStatus { + DRAFT = 'draft', + OPEN = 'open', + PAID = 'paid', + CANCELLED = 'cancelled', +} + +@Entity({ schema: 'financial', name: 'invoices' }) +export class Invoice { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', name: 'company_id' }) + companyId: string; + + @Column({ type: 'uuid', name: 'partner_id' }) + partnerId: string; + + @Column({ type: 'enum', enum: InvoiceType, name: 'invoice_type' }) + invoiceType: InvoiceType; + + @Column({ type: 'varchar', length: 100, nullable: true }) + number: string | null; + + @Column({ type: 'varchar', length: 255, nullable: true }) + ref: string | null; + + @Column({ type: 'date', name: 'invoice_date' }) + invoiceDate: Date; + + @Column({ type: 'date', nullable: true, name: 'due_date' }) + dueDate: Date | null; + + @Column({ type: 'uuid', name: 'currency_id' }) + currencyId: string; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0, name: 'amount_untaxed' }) + amountUntaxed: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0, name: 'amount_tax' }) + amountTax: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0, name: 'amount_total' }) + amountTotal: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0, name: 'amount_paid' }) + amountPaid: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0, name: 'amount_residual' }) + amountResidual: number; + + @Column({ type: 'enum', enum: InvoiceStatus, default: InvoiceStatus.DRAFT }) + status: InvoiceStatus; + + @Column({ type: 'uuid', nullable: true, name: 'payment_term_id' }) + paymentTermId: string | null; + + @Column({ type: 'uuid', nullable: true, name: 'journal_id' }) + journalId: string | null; + + @Column({ type: 'uuid', nullable: true, name: 'journal_entry_id' }) + journalEntryId: string | null; + + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @OneToMany(() => InvoiceLine, (line) => line.invoice, { cascade: true }) + lines: InvoiceLine[]; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', nullable: true }) + updatedAt: Date | null; +} +``` + +### InvoiceLine Entity + +```typescript +@Entity({ schema: 'financial', name: 'invoice_lines' }) +export class InvoiceLine { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'invoice_id' }) + invoiceId: string; + + @Column({ type: 'uuid', name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', nullable: true, name: 'product_id' }) + productId: string | null; + + @Column({ type: 'text' }) + description: string; + + @Column({ type: 'decimal', precision: 15, scale: 4 }) + quantity: number; + + @Column({ type: 'uuid', nullable: true, name: 'uom_id' }) + uomId: string | null; + + @Column({ type: 'decimal', precision: 15, scale: 2, name: 'price_unit' }) + priceUnit: number; + + @Column({ type: 'uuid', array: true, default: '{}', name: 'tax_ids' }) + taxIds: string[]; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0, name: 'amount_untaxed' }) + amountUntaxed: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0, name: 'amount_tax' }) + amountTax: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0, name: 'amount_total' }) + amountTotal: number; + + @Column({ type: 'uuid', nullable: true, name: 'account_id' }) + accountId: string | null; + + @ManyToOne(() => Invoice, (invoice) => invoice.lines, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'invoice_id' }) + invoice: Invoice; + + @ManyToOne(() => Account) + @JoinColumn({ name: 'account_id' }) + account: Account | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} +``` + +### Payment Entity + +```typescript +export enum PaymentType { + INBOUND = 'inbound', + OUTBOUND = 'outbound', +} + +export enum PaymentMethod { + CASH = 'cash', + BANK_TRANSFER = 'bank_transfer', + CHECK = 'check', + CARD = 'card', + OTHER = 'other', +} + +export enum PaymentStatus { + DRAFT = 'draft', + POSTED = 'posted', + RECONCILED = 'reconciled', + CANCELLED = 'cancelled', +} + +@Entity({ schema: 'financial', name: 'payments' }) +export class Payment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', name: 'company_id' }) + companyId: string; + + @Column({ type: 'uuid', name: 'partner_id' }) + partnerId: string; + + @Column({ type: 'enum', enum: PaymentType, name: 'payment_type' }) + paymentType: PaymentType; + + @Column({ type: 'enum', enum: PaymentMethod, name: 'payment_method' }) + paymentMethod: PaymentMethod; + + @Column({ type: 'decimal', precision: 15, scale: 2 }) + amount: number; + + @Column({ type: 'uuid', name: 'currency_id' }) + currencyId: string; + + @Column({ type: 'date', name: 'payment_date' }) + paymentDate: Date; + + @Column({ type: 'varchar', length: 255, nullable: true }) + ref: string | null; + + @Column({ type: 'enum', enum: PaymentStatus, default: PaymentStatus.DRAFT }) + status: PaymentStatus; + + @Column({ type: 'uuid', name: 'journal_id' }) + journalId: string; + + @Column({ type: 'uuid', nullable: true, name: 'journal_entry_id' }) + journalEntryId: string | null; + + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', nullable: true }) + updatedAt: Date | null; + + @Column({ type: 'timestamp', nullable: true, name: 'posted_at' }) + postedAt: Date | null; +} +``` + +### PaymentInvoice Entity + +```typescript +@Entity({ schema: 'financial', name: 'payment_invoice' }) +export class PaymentInvoice { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'payment_id' }) + paymentId: string; + + @Column({ type: 'uuid', name: 'invoice_id' }) + invoiceId: string; + + @Column({ type: 'decimal', precision: 15, scale: 2 }) + amount: number; + + @ManyToOne(() => Payment) + @JoinColumn({ name: 'payment_id' }) + payment: Payment; + + @ManyToOne(() => Invoice) + @JoinColumn({ name: 'invoice_id' }) + invoice: Invoice; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} +``` + +### PaymentMethodCatalog Entity + +```typescript +@Entity({ schema: 'financial', name: 'payment_methods' }) +export class PaymentMethodCatalog { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 255 }) + name: string; + + @Column({ type: 'varchar', length: 50 }) + code: string; + + @Column({ type: 'varchar', length: 20, name: 'payment_type' }) + paymentType: string; // 'inbound' | 'outbound' + + @Column({ type: 'boolean', default: true, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} +``` + +### PaymentTerm Entity + +```typescript +@Entity({ schema: 'financial', name: 'payment_terms' }) +export class PaymentTerm { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', name: 'company_id' }) + companyId: string; + + @Column({ type: 'varchar', length: 100 }) + name: string; + + @Column({ type: 'varchar', length: 20 }) + code: string; + + @Column({ type: 'jsonb', default: '[]' }) + terms: object; + + @Column({ type: 'boolean', default: true }) + active: boolean; + + @OneToMany(() => PaymentTermLine, (line) => line.paymentTerm, { cascade: true }) + lines: PaymentTermLine[]; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', nullable: true }) + updatedAt: Date | null; +} +``` + +### PaymentTermLine Entity + +```typescript +export type PaymentTermValue = 'balance' | 'percent' | 'fixed'; + +@Entity({ schema: 'financial', name: 'payment_term_lines' }) +export class PaymentTermLine { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'payment_term_id' }) + paymentTermId: string; + + @Column({ type: 'varchar', length: 20, default: 'balance' }) + value: PaymentTermValue; + + @Column({ type: 'decimal', precision: 20, scale: 6, name: 'value_amount', default: 0 }) + valueAmount: number; + + @Column({ type: 'int', name: 'nb_days', default: 0 }) + nbDays: number; + + @Column({ type: 'varchar', length: 20, name: 'delay_type', default: 'days_after' }) + delayType: string; + + @Column({ type: 'int', name: 'day_of_the_month', nullable: true }) + dayOfTheMonth: number | null; + + @Column({ type: 'int', default: 10 }) + sequence: number; + + @ManyToOne(() => PaymentTerm, (term) => term.lines, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'payment_term_id' }) + paymentTerm: PaymentTerm; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} +``` + +### JournalEntryV2 Entity + +```typescript +export enum EntryStatus { + DRAFT = 'draft', + POSTED = 'posted', + CANCELLED = 'cancelled', +} + +@Entity({ schema: 'financial', name: 'journal_entries' }) +export class JournalEntryV2 { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', name: 'company_id' }) + companyId: string; + + @Column({ type: 'uuid', name: 'journal_id' }) + journalId: string; + + @Column({ type: 'varchar', length: 100 }) + name: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + ref: string | null; + + @Column({ type: 'date' }) + date: Date; + + @Column({ type: 'enum', enum: EntryStatus, default: EntryStatus.DRAFT }) + status: EntryStatus; + + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @Column({ type: 'uuid', nullable: true, name: 'fiscal_period_id' }) + fiscalPeriodId: string | null; + + @OneToMany(() => JournalEntryLine, (line) => line.entry, { cascade: true }) + lines: JournalEntryLine[]; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', nullable: true }) + updatedAt: Date | null; + + @Column({ type: 'timestamp', nullable: true, name: 'posted_at' }) + postedAt: Date | null; + + @Column({ type: 'uuid', nullable: true, name: 'posted_by' }) + postedBy: string | null; +} +``` + +### JournalEntryLine Entity + +```typescript +@Entity({ schema: 'financial', name: 'journal_entry_lines' }) +export class JournalEntryLine { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'entry_id' }) + entryId: string; + + @Column({ type: 'uuid', name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', name: 'account_id' }) + accountId: string; + + @Column({ type: 'uuid', nullable: true, name: 'partner_id' }) + partnerId: string | null; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + debit: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + credit: number; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'varchar', length: 255, nullable: true }) + ref: string | null; + + @ManyToOne(() => JournalEntryV2, (entry) => entry.lines, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'entry_id' }) + entry: JournalEntryV2; + + @ManyToOne(() => Account) + @JoinColumn({ name: 'account_id' }) + account: Account; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} +``` + +### ReconcileModel Entity + +```typescript +export type ReconcileModelType = 'writeoff_button' | 'writeoff_suggestion' | 'invoice_matching'; + +@Entity({ schema: 'financial', name: 'reconcile_models' }) +export class ReconcileModel { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'varchar', length: 255 }) + name: string; + + @Column({ type: 'int', default: 10 }) + sequence: number; + + @Column({ type: 'varchar', length: 30, name: 'rule_type', default: 'writeoff_button' }) + ruleType: ReconcileModelType; + + @Column({ type: 'boolean', name: 'auto_reconcile', default: false }) + autoReconcile: boolean; + + @Column({ type: 'varchar', length: 20, name: 'match_nature', default: 'both' }) + matchNature: string; + + @Column({ type: 'varchar', length: 20, name: 'match_amount', default: 'any' }) + matchAmount: string; + + @Column({ type: 'decimal', precision: 20, scale: 6, name: 'match_amount_min', nullable: true }) + matchAmountMin: number | null; + + @Column({ type: 'decimal', precision: 20, scale: 6, name: 'match_amount_max', nullable: true }) + matchAmountMax: number | null; + + @Column({ type: 'varchar', length: 50, name: 'match_label', nullable: true }) + matchLabel: string | null; + + @Column({ type: 'varchar', length: 255, name: 'match_label_param', nullable: true }) + matchLabelParam: string | null; + + @Column({ type: 'boolean', name: 'match_partner', default: false }) + matchPartner: boolean; + + @Column({ type: 'uuid', array: true, name: 'match_partner_ids', nullable: true }) + matchPartnerIds: string[] | null; + + @Column({ type: 'boolean', name: 'is_active', default: true }) + isActive: boolean; + + @Column({ type: 'uuid', name: 'company_id', nullable: true }) + companyId: string | null; + + @OneToMany(() => ReconcileModelLine, (line) => line.reconcileModel, { cascade: true }) + lines: ReconcileModelLine[]; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', nullable: true }) + updatedAt: Date | null; +} +``` + +### ReconcileModelLine Entity + +```typescript +@Entity({ schema: 'financial', name: 'reconcile_model_lines' }) +export class ReconcileModelLine { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'model_id' }) + modelId: string; + + @Column({ type: 'int', default: 10 }) + sequence: number; + + @Column({ type: 'uuid', name: 'account_id' }) + accountId: string; + + @Column({ type: 'uuid', name: 'journal_id', nullable: true }) + journalId: string | null; + + @Column({ type: 'varchar', length: 255, nullable: true }) + label: string | null; + + @Column({ type: 'varchar', length: 20, name: 'amount_type', default: 'percentage' }) + amountType: string; + + @Column({ type: 'decimal', precision: 20, scale: 6, name: 'amount_value', default: 100 }) + amountValue: number; + + @Column({ type: 'uuid', array: true, name: 'tax_ids', nullable: true }) + taxIds: string[] | null; + + @Column({ type: 'uuid', name: 'analytic_account_id', nullable: true }) + analyticAccountId: string | null; + + @ManyToOne(() => ReconcileModel, (model) => model.lines, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'model_id' }) + reconcileModel: ReconcileModel; + + @ManyToOne(() => Account) + @JoinColumn({ name: 'account_id' }) + account: Account; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} +``` + +### Tax Entity + +```typescript +export enum TaxType { + SALES = 'sales', + PURCHASE = 'purchase', + ALL = 'all', +} + +@Entity({ schema: 'financial', name: 'taxes' }) +export class Tax { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid', name: 'tenant_id' }) + tenantId: string; + + @Column({ type: 'uuid', name: 'company_id' }) + companyId: string; + + @Column({ type: 'varchar', length: 100 }) + name: string; + + @Column({ type: 'varchar', length: 20 }) + code: string; + + @Column({ type: 'enum', enum: TaxType, name: 'tax_type' }) + taxType: TaxType; + + @Column({ type: 'decimal', precision: 5, scale: 2 }) + amount: number; + + @Column({ type: 'boolean', default: false, name: 'included_in_price' }) + includedInPrice: boolean; + + @Column({ type: 'boolean', default: true }) + active: boolean; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', nullable: true }) + updatedAt: Date | null; +} +``` + +### Incoterm Entity + +```typescript +@Entity({ schema: 'financial', name: 'incoterms' }) +export class Incoterm { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 255 }) + name: string; + + @Column({ type: 'varchar', length: 10, unique: true }) + code: string; + + @Column({ type: 'boolean', default: true, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} +``` + --- ## Servicios @@ -943,6 +1603,1510 @@ export class FiscalPeriodsService { } ``` +### InvoicesService + +```typescript +@Injectable() +export class InvoicesService { + constructor( + @InjectRepository(Invoice) + private readonly invoiceRepo: Repository, + @InjectRepository(InvoiceLine) + private readonly lineRepo: Repository, + private readonly taxesService: TaxesService, + private readonly journalEntriesService: JournalEntriesService, + @InjectDataSource() + private readonly dataSource: DataSource, + ) {} + + async findAll( + tenantId: string, + query: QueryInvoiceDto + ): Promise> { + const qb = this.invoiceRepo.createQueryBuilder('inv') + .where('inv.tenant_id = :tenantId', { tenantId }) + .leftJoinAndSelect('inv.lines', 'lines'); + + if (query.invoiceType) { + qb.andWhere('inv.invoice_type = :type', { type: query.invoiceType }); + } + + if (query.status) { + qb.andWhere('inv.status = :status', { status: query.status }); + } + + if (query.partnerId) { + qb.andWhere('inv.partner_id = :partnerId', { partnerId: query.partnerId }); + } + + if (query.dateFrom) { + qb.andWhere('inv.invoice_date >= :dateFrom', { dateFrom: query.dateFrom }); + } + + if (query.dateTo) { + qb.andWhere('inv.invoice_date <= :dateTo', { dateTo: query.dateTo }); + } + + qb.orderBy('inv.invoice_date', 'DESC') + .addOrderBy('inv.number', 'DESC'); + + return paginate(qb, query); + } + + async findById(id: string): Promise { + return this.invoiceRepo.findOne({ + where: { id }, + relations: ['lines', 'lines.account', 'journal', 'journalEntry'], + }); + } + + async create( + tenantId: string, + companyId: string, + userId: string, + dto: CreateInvoiceDto + ): Promise { + const lines = await this.calculateLineAmounts(dto.lines); + const totals = this.calculateInvoiceTotals(lines); + + const invoice = this.invoiceRepo.create({ + tenantId, + companyId, + partnerId: dto.partnerId, + invoiceType: dto.invoiceType, + invoiceDate: dto.invoiceDate, + dueDate: dto.dueDate, + currencyId: dto.currencyId, + paymentTermId: dto.paymentTermId, + journalId: dto.journalId, + notes: dto.notes, + ref: dto.ref, + ...totals, + amountResidual: totals.amountTotal, + createdBy: userId, + lines: lines.map((line, index) => ({ + ...line, + tenantId, + sequence: index + 1, + })), + }); + + return this.invoiceRepo.save(invoice); + } + + async update( + id: string, + userId: string, + dto: UpdateInvoiceDto + ): Promise { + const invoice = await this.invoiceRepo.findOneOrFail({ + where: { id }, + relations: ['lines'], + }); + + if (invoice.status !== InvoiceStatus.DRAFT) { + throw new BadRequestException('Only draft invoices can be updated'); + } + + if (dto.lines) { + await this.lineRepo.delete({ invoiceId: id }); + const lines = await this.calculateLineAmounts(dto.lines); + const totals = this.calculateInvoiceTotals(lines); + + Object.assign(invoice, dto, totals, { + amountResidual: totals.amountTotal, + updatedBy: userId, + lines: lines.map((line, index) => ({ + ...line, + invoiceId: id, + tenantId: invoice.tenantId, + sequence: index + 1, + })), + }); + } else { + Object.assign(invoice, dto, { updatedBy: userId }); + } + + return this.invoiceRepo.save(invoice); + } + + async validate(id: string, userId: string): Promise { + return this.dataSource.transaction(async manager => { + const invoice = await manager.findOne(Invoice, { + where: { id }, + relations: ['lines'], + lock: { mode: 'pessimistic_write' }, + }); + + if (!invoice) { + throw new NotFoundException('Invoice not found'); + } + + if (invoice.status !== InvoiceStatus.DRAFT) { + throw new BadRequestException('Invoice is not in draft status'); + } + + // Generate invoice number + const number = await this.generateInvoiceNumber( + manager, + invoice.tenantId, + invoice.invoiceType + ); + + // Create journal entry + const journalEntry = await this.createJournalEntry(manager, invoice, userId); + + invoice.number = number; + invoice.status = InvoiceStatus.OPEN; + invoice.journalEntryId = journalEntry.id; + invoice.validatedAt = new Date(); + invoice.validatedBy = userId; + + return manager.save(invoice); + }); + } + + async cancel(id: string, userId: string): Promise { + const invoice = await this.invoiceRepo.findOneOrFail({ where: { id } }); + + if (invoice.status === InvoiceStatus.CANCELLED) { + throw new BadRequestException('Invoice is already cancelled'); + } + + if (invoice.amountPaid > 0) { + throw new BadRequestException('Cannot cancel invoice with payments'); + } + + invoice.status = InvoiceStatus.CANCELLED; + invoice.cancelledAt = new Date(); + invoice.cancelledBy = userId; + + return this.invoiceRepo.save(invoice); + } + + async registerPayment( + invoiceId: string, + amount: number + ): Promise { + const invoice = await this.invoiceRepo.findOneOrFail({ where: { id: invoiceId } }); + + invoice.amountPaid += amount; + invoice.amountResidual = invoice.amountTotal - invoice.amountPaid; + + if (invoice.amountResidual <= 0) { + invoice.status = InvoiceStatus.PAID; + } + + return this.invoiceRepo.save(invoice); + } + + private async calculateLineAmounts(lines: CreateInvoiceLineDto[]): Promise { + return Promise.all(lines.map(async line => { + const amountUntaxed = line.quantity * line.priceUnit; + let amountTax = 0; + + if (line.taxIds?.length > 0) { + // Calculate taxes + for (const taxId of line.taxIds) { + const tax = await this.taxesService.findById(taxId); + amountTax += amountUntaxed * (tax.amount / 100); + } + } + + return { + ...line, + amountUntaxed, + amountTax, + amountTotal: amountUntaxed + amountTax, + } as InvoiceLine; + })); + } + + private calculateInvoiceTotals(lines: InvoiceLine[]): { + amountUntaxed: number; + amountTax: number; + amountTotal: number; + } { + return lines.reduce( + (totals, line) => ({ + amountUntaxed: totals.amountUntaxed + line.amountUntaxed, + amountTax: totals.amountTax + line.amountTax, + amountTotal: totals.amountTotal + line.amountTotal, + }), + { amountUntaxed: 0, amountTax: 0, amountTotal: 0 } + ); + } + + private async generateInvoiceNumber( + manager: EntityManager, + tenantId: string, + type: InvoiceType + ): Promise { + const prefix = type === InvoiceType.CUSTOMER ? 'INV' : 'BILL'; + const year = new Date().getFullYear(); + const pattern = `${prefix}-${year}-%`; + + const lastInvoice = await manager.findOne(Invoice, { + where: { tenantId, number: Like(pattern) }, + order: { number: 'DESC' }, + }); + + let sequence = 1; + if (lastInvoice) { + const lastNum = parseInt(lastInvoice.number.split('-').pop()); + sequence = lastNum + 1; + } + + return `${prefix}-${year}-${sequence.toString().padStart(6, '0')}`; + } + + private async createJournalEntry( + manager: EntityManager, + invoice: Invoice, + userId: string + ): Promise { + // Implementation creates corresponding journal entry + // for the invoice based on configured accounts + } +} +``` + +### PaymentsService + +```typescript +/** + * PaymentsService - Gestion de pagos con reconciliacion + * Tests: 36 tests unitarios + */ +@Injectable() +export class PaymentsService { + constructor( + @InjectRepository(Payment) + private readonly paymentRepo: Repository, + @InjectRepository(PaymentInvoice) + private readonly paymentInvoiceRepo: Repository, + private readonly invoicesService: InvoicesService, + private readonly journalEntriesService: JournalEntriesService, + @InjectDataSource() + private readonly dataSource: DataSource, + ) {} + + async findAll( + tenantId: string, + query: QueryPaymentDto + ): Promise> { + const qb = this.paymentRepo.createQueryBuilder('p') + .where('p.tenant_id = :tenantId', { tenantId }) + .leftJoinAndSelect('p.journal', 'journal'); + + if (query.paymentType) { + qb.andWhere('p.payment_type = :type', { type: query.paymentType }); + } + + if (query.status) { + qb.andWhere('p.status = :status', { status: query.status }); + } + + if (query.partnerId) { + qb.andWhere('p.partner_id = :partnerId', { partnerId: query.partnerId }); + } + + if (query.dateFrom) { + qb.andWhere('p.payment_date >= :dateFrom', { dateFrom: query.dateFrom }); + } + + if (query.dateTo) { + qb.andWhere('p.payment_date <= :dateTo', { dateTo: query.dateTo }); + } + + qb.orderBy('p.payment_date', 'DESC'); + + return paginate(qb, query); + } + + async findById(id: string): Promise { + return this.paymentRepo.findOne({ + where: { id }, + relations: ['journal', 'journalEntry'], + }); + } + + async create( + tenantId: string, + companyId: string, + userId: string, + dto: CreatePaymentDto + ): Promise { + const payment = this.paymentRepo.create({ + tenantId, + companyId, + partnerId: dto.partnerId, + paymentType: dto.paymentType, + paymentMethod: dto.paymentMethod, + amount: dto.amount, + currencyId: dto.currencyId, + paymentDate: dto.paymentDate, + journalId: dto.journalId, + ref: dto.ref, + notes: dto.notes, + createdBy: userId, + }); + + return this.paymentRepo.save(payment); + } + + async update( + id: string, + userId: string, + dto: UpdatePaymentDto + ): Promise { + const payment = await this.paymentRepo.findOneOrFail({ where: { id } }); + + if (payment.status !== PaymentStatus.DRAFT) { + throw new BadRequestException('Only draft payments can be updated'); + } + + Object.assign(payment, dto, { updatedBy: userId }); + return this.paymentRepo.save(payment); + } + + async post(id: string, userId: string): Promise { + return this.dataSource.transaction(async manager => { + const payment = await manager.findOne(Payment, { + where: { id }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!payment) { + throw new NotFoundException('Payment not found'); + } + + if (payment.status !== PaymentStatus.DRAFT) { + throw new BadRequestException('Payment is not in draft status'); + } + + // Create journal entry for payment + const journalEntry = await this.createPaymentJournalEntry( + manager, + payment, + userId + ); + + payment.status = PaymentStatus.POSTED; + payment.journalEntryId = journalEntry.id; + payment.postedAt = new Date(); + payment.postedBy = userId; + + return manager.save(payment); + }); + } + + async cancel(id: string, userId: string): Promise { + const payment = await this.paymentRepo.findOneOrFail({ where: { id } }); + + if (payment.status === PaymentStatus.CANCELLED) { + throw new BadRequestException('Payment is already cancelled'); + } + + if (payment.status === PaymentStatus.RECONCILED) { + throw new BadRequestException('Cannot cancel reconciled payment'); + } + + // Reverse invoice allocations + await this.reverseInvoiceAllocations(payment); + + payment.status = PaymentStatus.CANCELLED; + + return this.paymentRepo.save(payment); + } + + /** + * Reconcilia un pago con una o mas facturas + */ + async reconcile( + paymentId: string, + invoiceAllocations: InvoiceAllocationDto[] + ): Promise { + return this.dataSource.transaction(async manager => { + const payment = await manager.findOne(Payment, { + where: { id: paymentId }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!payment) { + throw new NotFoundException('Payment not found'); + } + + if (payment.status !== PaymentStatus.POSTED) { + throw new BadRequestException('Payment must be posted to reconcile'); + } + + // Validate total allocation amount + const totalAllocation = invoiceAllocations.reduce( + (sum, alloc) => sum + alloc.amount, + 0 + ); + + if (totalAllocation > payment.amount) { + throw new BadRequestException('Total allocation exceeds payment amount'); + } + + // Create payment-invoice records and update invoices + for (const allocation of invoiceAllocations) { + const invoice = await manager.findOne(Invoice, { + where: { id: allocation.invoiceId }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!invoice) { + throw new NotFoundException(`Invoice ${allocation.invoiceId} not found`); + } + + if (allocation.amount > invoice.amountResidual) { + throw new BadRequestException( + `Allocation amount exceeds invoice residual for ${invoice.number}` + ); + } + + // Create payment-invoice link + const paymentInvoice = manager.create(PaymentInvoice, { + paymentId, + invoiceId: allocation.invoiceId, + amount: allocation.amount, + }); + await manager.save(paymentInvoice); + + // Update invoice amounts + invoice.amountPaid += allocation.amount; + invoice.amountResidual -= allocation.amount; + + if (invoice.amountResidual <= 0) { + invoice.status = InvoiceStatus.PAID; + } + + await manager.save(invoice); + } + + // Mark payment as reconciled if fully allocated + if (totalAllocation >= payment.amount) { + payment.status = PaymentStatus.RECONCILED; + } + + return manager.save(payment); + }); + } + + /** + * Obtiene facturas pendientes de un partner para reconciliar + */ + async getUnreconciledInvoices( + tenantId: string, + partnerId: string, + paymentType: PaymentType + ): Promise { + const invoiceType = paymentType === PaymentType.INBOUND + ? InvoiceType.CUSTOMER + : InvoiceType.SUPPLIER; + + return this.invoicesService.findAll(tenantId, { + partnerId, + invoiceType, + status: InvoiceStatus.OPEN, + }); + } + + /** + * Auto-reconcilia pagos con facturas basado en reglas + */ + async autoReconcile( + tenantId: string, + paymentId: string + ): Promise { + const payment = await this.findById(paymentId); + + // Find matching invoices by partner and amount + const invoices = await this.getUnreconciledInvoices( + tenantId, + payment.partnerId, + payment.paymentType + ); + + // Try exact match first + const exactMatch = invoices.find(inv => inv.amountResidual === payment.amount); + if (exactMatch) { + return this.reconcile(paymentId, [ + { invoiceId: exactMatch.id, amount: payment.amount } + ]); + } + + // Try to allocate to multiple invoices + const allocations: InvoiceAllocationDto[] = []; + let remainingAmount = payment.amount; + + for (const invoice of invoices) { + if (remainingAmount <= 0) break; + + const allocAmount = Math.min(remainingAmount, invoice.amountResidual); + allocations.push({ invoiceId: invoice.id, amount: allocAmount }); + remainingAmount -= allocAmount; + } + + if (allocations.length > 0) { + return this.reconcile(paymentId, allocations); + } + + return payment; + } + + private async reverseInvoiceAllocations(payment: Payment): Promise { + const allocations = await this.paymentInvoiceRepo.find({ + where: { paymentId: payment.id }, + }); + + for (const allocation of allocations) { + const invoice = await this.invoicesService.findById(allocation.invoiceId); + invoice.amountPaid -= allocation.amount; + invoice.amountResidual += allocation.amount; + + if (invoice.status === InvoiceStatus.PAID) { + invoice.status = InvoiceStatus.OPEN; + } + + await this.invoicesService.update(invoice.id, null, invoice); + } + + await this.paymentInvoiceRepo.delete({ paymentId: payment.id }); + } + + private async createPaymentJournalEntry( + manager: EntityManager, + payment: Payment, + userId: string + ): Promise { + // Implementation creates journal entry for payment + // based on payment type and configured accounts + } +} +``` + +### PaymentMethodsService + +```typescript +@Injectable() +export class PaymentMethodsService { + constructor( + @InjectRepository(PaymentMethodCatalog) + private readonly repo: Repository, + ) {} + + async findAll(query?: QueryPaymentMethodDto): Promise { + const qb = this.repo.createQueryBuilder('pm'); + + if (query?.paymentType) { + qb.andWhere('pm.payment_type = :type', { type: query.paymentType }); + } + + if (query?.isActive !== undefined) { + qb.andWhere('pm.is_active = :isActive', { isActive: query.isActive }); + } + + return qb.orderBy('pm.name', 'ASC').getMany(); + } + + async findById(id: string): Promise { + return this.repo.findOneOrFail({ where: { id } }); + } + + async findByCode(code: string, paymentType: string): Promise { + return this.repo.findOne({ + where: { code, paymentType }, + }); + } + + async create(dto: CreatePaymentMethodDto): Promise { + const existing = await this.findByCode(dto.code, dto.paymentType); + if (existing) { + throw new ConflictException('Payment method code already exists for this type'); + } + + const method = this.repo.create(dto); + return this.repo.save(method); + } + + async update(id: string, dto: UpdatePaymentMethodDto): Promise { + const method = await this.repo.findOneOrFail({ where: { id } }); + Object.assign(method, dto); + return this.repo.save(method); + } + + async deactivate(id: string): Promise { + const method = await this.repo.findOneOrFail({ where: { id } }); + method.isActive = false; + return this.repo.save(method); + } +} +``` + +### PaymentTermsService + +```typescript +@Injectable() +export class PaymentTermsService { + constructor( + @InjectRepository(PaymentTerm) + private readonly termRepo: Repository, + @InjectRepository(PaymentTermLine) + private readonly lineRepo: Repository, + ) {} + + async findAll( + tenantId: string, + companyId: string + ): Promise { + return this.termRepo.find({ + where: { tenantId, companyId, active: true }, + relations: ['lines'], + order: { name: 'ASC' }, + }); + } + + async findById(id: string): Promise { + return this.termRepo.findOne({ + where: { id }, + relations: ['lines'], + }); + } + + async create( + tenantId: string, + companyId: string, + userId: string, + dto: CreatePaymentTermDto + ): Promise { + // Validate lines sum to 100% for percentage types + const percentageLines = dto.lines.filter(l => l.value === 'percent'); + if (percentageLines.length > 0) { + const totalPercent = percentageLines.reduce((sum, l) => sum + l.valueAmount, 0); + if (Math.abs(totalPercent - 100) > 0.01) { + throw new BadRequestException('Percentage lines must sum to 100%'); + } + } + + const term = this.termRepo.create({ + tenantId, + companyId, + name: dto.name, + code: dto.code, + createdBy: userId, + lines: dto.lines.map((line, index) => ({ + ...line, + sequence: index * 10 + 10, + })), + }); + + return this.termRepo.save(term); + } + + async update( + id: string, + userId: string, + dto: UpdatePaymentTermDto + ): Promise { + const term = await this.termRepo.findOneOrFail({ + where: { id }, + relations: ['lines'], + }); + + if (dto.lines) { + await this.lineRepo.delete({ paymentTermId: id }); + term.lines = dto.lines.map((line, index) => ({ + ...line, + paymentTermId: id, + sequence: index * 10 + 10, + } as PaymentTermLine)); + } + + Object.assign(term, dto, { updatedBy: userId }); + return this.termRepo.save(term); + } + + async deactivate(id: string): Promise { + const term = await this.termRepo.findOneOrFail({ where: { id } }); + term.active = false; + return this.termRepo.save(term); + } + + /** + * Calcula fechas de vencimiento basadas en terminos de pago + */ + async calculateDueDates( + paymentTermId: string, + invoiceDate: Date, + totalAmount: number + ): Promise { + const term = await this.findById(paymentTermId); + const dueDates: PaymentDueDate[] = []; + + let remainingAmount = totalAmount; + + for (const line of term.lines.sort((a, b) => a.sequence - b.sequence)) { + let lineAmount: number; + + switch (line.value) { + case 'balance': + lineAmount = remainingAmount; + break; + case 'percent': + lineAmount = totalAmount * (line.valueAmount / 100); + break; + case 'fixed': + lineAmount = Math.min(line.valueAmount, remainingAmount); + break; + } + + const dueDate = this.calculateDueDate(invoiceDate, line); + + dueDates.push({ + dueDate, + amount: lineAmount, + sequence: line.sequence, + }); + + remainingAmount -= lineAmount; + if (remainingAmount <= 0) break; + } + + return dueDates; + } + + private calculateDueDate( + invoiceDate: Date, + line: PaymentTermLine + ): Date { + let baseDate = new Date(invoiceDate); + + switch (line.delayType) { + case 'days_after': + baseDate.setDate(baseDate.getDate() + line.nbDays); + break; + case 'days_after_end_of_month': + baseDate = endOfMonth(baseDate); + baseDate.setDate(baseDate.getDate() + line.nbDays); + break; + case 'days_after_end_of_next_month': + baseDate = addMonths(baseDate, 1); + baseDate = endOfMonth(baseDate); + baseDate.setDate(baseDate.getDate() + line.nbDays); + break; + } + + if (line.dayOfTheMonth) { + const targetDay = Math.min(line.dayOfTheMonth, getDaysInMonth(baseDate)); + baseDate.setDate(targetDay); + } + + return baseDate; + } +} +``` + +### JournalEntriesService + +```typescript +/** + * JournalEntriesService - Servicio para asientos contables + * Incluye funcionalidad de posting de asientos + */ +@Injectable() +export class JournalEntriesService { + constructor( + @InjectRepository(JournalEntry) + private readonly entryRepo: Repository, + @InjectRepository(JournalEntryLine) + private readonly lineRepo: Repository, + private readonly accountsService: AccountsService, + private readonly periodsService: FiscalPeriodsService, + @InjectDataSource() + private readonly dataSource: DataSource, + ) {} + + async findAll( + tenantId: string, + query: QueryJournalEntryDto + ): Promise> { + const qb = this.entryRepo.createQueryBuilder('je') + .where('je.tenant_id = :tenantId', { tenantId }) + .leftJoinAndSelect('je.lines', 'lines') + .leftJoinAndSelect('lines.account', 'account') + .leftJoinAndSelect('je.journal', 'journal'); + + if (query.journalId) { + qb.andWhere('je.journal_id = :journalId', { journalId: query.journalId }); + } + + if (query.status) { + qb.andWhere('je.status = :status', { status: query.status }); + } + + if (query.dateFrom) { + qb.andWhere('je.date >= :dateFrom', { dateFrom: query.dateFrom }); + } + + if (query.dateTo) { + qb.andWhere('je.date <= :dateTo', { dateTo: query.dateTo }); + } + + qb.orderBy('je.date', 'DESC') + .addOrderBy('je.name', 'DESC'); + + return paginate(qb, query); + } + + async findById(id: string): Promise { + return this.entryRepo.findOne({ + where: { id }, + relations: ['lines', 'lines.account', 'journal'], + }); + } + + async create( + tenantId: string, + companyId: string, + userId: string, + dto: CreateJournalEntryDto + ): Promise { + // Validate debit = credit + const totalDebit = dto.lines.reduce((sum, l) => sum + (l.debit || 0), 0); + const totalCredit = dto.lines.reduce((sum, l) => sum + (l.credit || 0), 0); + + if (Math.abs(totalDebit - totalCredit) > 0.001) { + throw new BadRequestException( + `Entry is not balanced: debit=${totalDebit} credit=${totalCredit}` + ); + } + + // Generate entry name + const name = await this.generateEntryName(tenantId, dto.journalId); + + const entry = this.entryRepo.create({ + tenantId, + companyId, + journalId: dto.journalId, + name, + date: dto.date, + ref: dto.ref, + notes: dto.notes, + createdBy: userId, + lines: dto.lines.map(line => ({ + ...line, + tenantId, + })), + }); + + return this.entryRepo.save(entry); + } + + async update( + id: string, + userId: string, + dto: UpdateJournalEntryDto + ): Promise { + const entry = await this.entryRepo.findOneOrFail({ + where: { id }, + relations: ['lines'], + }); + + if (entry.status !== EntryStatus.DRAFT) { + throw new BadRequestException('Only draft entries can be updated'); + } + + if (dto.lines) { + // Validate balance + const totalDebit = dto.lines.reduce((sum, l) => sum + (l.debit || 0), 0); + const totalCredit = dto.lines.reduce((sum, l) => sum + (l.credit || 0), 0); + + if (Math.abs(totalDebit - totalCredit) > 0.001) { + throw new BadRequestException('Entry is not balanced'); + } + + await this.lineRepo.delete({ entryId: id }); + entry.lines = dto.lines.map(line => ({ + ...line, + entryId: id, + tenantId: entry.tenantId, + } as JournalEntryLine)); + } + + Object.assign(entry, dto, { updatedBy: userId }); + return this.entryRepo.save(entry); + } + + /** + * Postea un asiento contable, actualizando saldos de cuentas + */ + async post(id: string, userId: string): Promise { + return this.dataSource.transaction(async manager => { + const entry = await manager.findOne(JournalEntry, { + where: { id }, + relations: ['lines', 'lines.account'], + lock: { mode: 'pessimistic_write' }, + }); + + if (!entry) { + throw new NotFoundException('Journal entry not found'); + } + + if (entry.status !== EntryStatus.DRAFT) { + throw new BadRequestException('Entry is not in draft status'); + } + + // Validate fiscal period is open + if (entry.fiscalPeriodId) { + const period = await manager.findOne(FiscalPeriod, { + where: { id: entry.fiscalPeriodId }, + }); + + if (period?.status !== 'open') { + throw new BadRequestException('Fiscal period is not open'); + } + } + + // Update account balances + for (const line of entry.lines) { + const account = await manager.findOne(Account, { + where: { id: line.accountId }, + relations: ['accountType'], + lock: { mode: 'pessimistic_write' }, + }); + + const balanceChange = account.accountType.normalBalance === 'debit' + ? line.debit - line.credit + : line.credit - line.debit; + + await manager.update(Account, line.accountId, { + currentBalance: () => `current_balance + ${balanceChange}`, + updatedAt: new Date(), + }); + } + + entry.status = EntryStatus.POSTED; + entry.postedAt = new Date(); + entry.postedBy = userId; + + return manager.save(entry); + }); + } + + /** + * Cancela un asiento contable, revirtiendo saldos + */ + async cancel(id: string, userId: string): Promise { + return this.dataSource.transaction(async manager => { + const entry = await manager.findOne(JournalEntry, { + where: { id }, + relations: ['lines', 'lines.account'], + lock: { mode: 'pessimistic_write' }, + }); + + if (!entry) { + throw new NotFoundException('Journal entry not found'); + } + + if (entry.status === EntryStatus.CANCELLED) { + throw new BadRequestException('Entry is already cancelled'); + } + + // Reverse account balances if entry was posted + if (entry.status === EntryStatus.POSTED) { + for (const line of entry.lines) { + const account = await manager.findOne(Account, { + where: { id: line.accountId }, + relations: ['accountType'], + lock: { mode: 'pessimistic_write' }, + }); + + const balanceChange = account.accountType.normalBalance === 'debit' + ? line.credit - line.debit // Reverse + : line.debit - line.credit; + + await manager.update(Account, line.accountId, { + currentBalance: () => `current_balance + ${balanceChange}`, + updatedAt: new Date(), + }); + } + } + + entry.status = EntryStatus.CANCELLED; + entry.cancelledAt = new Date(); + entry.cancelledBy = userId; + + return manager.save(entry); + }); + } + + private async generateEntryName( + tenantId: string, + journalId: string + ): Promise { + const journal = await this.dataSource.manager.findOne(Journal, { + where: { id: journalId }, + }); + + const year = new Date().getFullYear(); + const month = (new Date().getMonth() + 1).toString().padStart(2, '0'); + const prefix = `${journal.code}/${year}/${month}/`; + + const lastEntry = await this.entryRepo.findOne({ + where: { tenantId, name: Like(`${prefix}%`) }, + order: { name: 'DESC' }, + }); + + let sequence = 1; + if (lastEntry) { + const lastNum = parseInt(lastEntry.name.split('/').pop()); + sequence = lastNum + 1; + } + + return `${prefix}${sequence.toString().padStart(4, '0')}`; + } +} +``` + +### ReconcileModelsService + +```typescript +@Injectable() +export class ReconcileModelsService { + constructor( + @InjectRepository(ReconcileModel) + private readonly modelRepo: Repository, + @InjectRepository(ReconcileModelLine) + private readonly lineRepo: Repository, + ) {} + + async findAll( + tenantId: string, + companyId?: string + ): Promise { + const where: any = { tenantId, isActive: true }; + if (companyId) { + where.companyId = companyId; + } + + return this.modelRepo.find({ + where, + relations: ['lines', 'lines.account'], + order: { sequence: 'ASC' }, + }); + } + + async findById(id: string): Promise { + return this.modelRepo.findOne({ + where: { id }, + relations: ['lines', 'lines.account', 'lines.journal'], + }); + } + + async create( + tenantId: string, + dto: CreateReconcileModelDto + ): Promise { + const model = this.modelRepo.create({ + tenantId, + companyId: dto.companyId, + name: dto.name, + sequence: dto.sequence || 10, + ruleType: dto.ruleType, + autoReconcile: dto.autoReconcile, + matchNature: dto.matchNature, + matchAmount: dto.matchAmount, + matchAmountMin: dto.matchAmountMin, + matchAmountMax: dto.matchAmountMax, + matchLabel: dto.matchLabel, + matchLabelParam: dto.matchLabelParam, + matchPartner: dto.matchPartner, + matchPartnerIds: dto.matchPartnerIds, + lines: dto.lines?.map((line, index) => ({ + ...line, + sequence: index * 10 + 10, + })), + }); + + return this.modelRepo.save(model); + } + + async update( + id: string, + dto: UpdateReconcileModelDto + ): Promise { + const model = await this.modelRepo.findOneOrFail({ + where: { id }, + relations: ['lines'], + }); + + if (dto.lines) { + await this.lineRepo.delete({ modelId: id }); + model.lines = dto.lines.map((line, index) => ({ + ...line, + modelId: id, + sequence: index * 10 + 10, + } as ReconcileModelLine)); + } + + Object.assign(model, dto); + return this.modelRepo.save(model); + } + + async deactivate(id: string): Promise { + const model = await this.modelRepo.findOneOrFail({ where: { id } }); + model.isActive = false; + return this.modelRepo.save(model); + } + + /** + * Encuentra modelos de reconciliacion que coincidan con una transaccion + */ + async findMatchingModels( + tenantId: string, + transaction: ReconcileTransaction + ): Promise { + const models = await this.findAll(tenantId, transaction.companyId); + + return models.filter(model => this.matchesModel(model, transaction)); + } + + /** + * Aplica un modelo de reconciliacion a una transaccion + */ + async applyModel( + modelId: string, + transaction: ReconcileTransaction + ): Promise { + const model = await this.findById(modelId); + + if (!this.matchesModel(model, transaction)) { + throw new BadRequestException('Model does not match transaction'); + } + + const entries: ReconcileEntryLine[] = []; + let remainingAmount = Math.abs(transaction.amount); + + for (const line of model.lines.sort((a, b) => a.sequence - b.sequence)) { + let lineAmount: number; + + switch (line.amountType) { + case 'percentage': + lineAmount = remainingAmount * (line.amountValue / 100); + break; + case 'fixed': + lineAmount = Math.min(line.amountValue, remainingAmount); + break; + default: + lineAmount = remainingAmount; + } + + entries.push({ + accountId: line.accountId, + journalId: line.journalId, + label: line.label, + amount: lineAmount, + taxIds: line.taxIds, + analyticAccountId: line.analyticAccountId, + }); + + remainingAmount -= lineAmount; + if (remainingAmount <= 0) break; + } + + return { + modelId, + entries, + autoReconcile: model.autoReconcile, + }; + } + + private matchesModel( + model: ReconcileModel, + transaction: ReconcileTransaction + ): boolean { + // Match nature (inbound/outbound/both) + if (model.matchNature !== 'both') { + const isInbound = transaction.amount > 0; + if (model.matchNature === 'amount_received' && !isInbound) return false; + if (model.matchNature === 'amount_paid' && isInbound) return false; + } + + // Match amount range + const amount = Math.abs(transaction.amount); + switch (model.matchAmount) { + case 'lower': + if (amount >= model.matchAmountMax) return false; + break; + case 'greater': + if (amount <= model.matchAmountMin) return false; + break; + case 'between': + if (amount < model.matchAmountMin || amount > model.matchAmountMax) return false; + break; + } + + // Match label + if (model.matchLabel && transaction.label) { + switch (model.matchLabel) { + case 'contains': + if (!transaction.label.includes(model.matchLabelParam)) return false; + break; + case 'not_contains': + if (transaction.label.includes(model.matchLabelParam)) return false; + break; + case 'match_regex': + const regex = new RegExp(model.matchLabelParam); + if (!regex.test(transaction.label)) return false; + break; + } + } + + // Match partner + if (model.matchPartner && model.matchPartnerIds?.length > 0) { + if (!model.matchPartnerIds.includes(transaction.partnerId)) return false; + } + + return true; + } +} +``` + +### TaxesService + +```typescript +@Injectable() +export class TaxesService { + constructor( + @InjectRepository(Tax) + private readonly repo: Repository, + ) {} + + async findAll( + tenantId: string, + companyId: string, + query?: QueryTaxDto + ): Promise { + const qb = this.repo.createQueryBuilder('t') + .where('t.tenant_id = :tenantId', { tenantId }) + .andWhere('t.company_id = :companyId', { companyId }); + + if (query?.taxType) { + qb.andWhere('t.tax_type IN (:...types)', { + types: query.taxType === TaxType.ALL + ? [TaxType.SALES, TaxType.PURCHASE, TaxType.ALL] + : [query.taxType, TaxType.ALL], + }); + } + + if (query?.active !== undefined) { + qb.andWhere('t.active = :active', { active: query.active }); + } + + return qb.orderBy('t.name', 'ASC').getMany(); + } + + async findById(id: string): Promise { + return this.repo.findOneOrFail({ where: { id } }); + } + + async findByCode( + tenantId: string, + code: string + ): Promise { + return this.repo.findOne({ where: { tenantId, code } }); + } + + async create( + tenantId: string, + companyId: string, + userId: string, + dto: CreateTaxDto + ): Promise { + const existing = await this.findByCode(tenantId, dto.code); + if (existing) { + throw new ConflictException('Tax code already exists'); + } + + const tax = this.repo.create({ + tenantId, + companyId, + name: dto.name, + code: dto.code, + taxType: dto.taxType, + amount: dto.amount, + includedInPrice: dto.includedInPrice || false, + createdBy: userId, + }); + + return this.repo.save(tax); + } + + async update( + id: string, + userId: string, + dto: UpdateTaxDto + ): Promise { + const tax = await this.repo.findOneOrFail({ where: { id } }); + + if (dto.code && dto.code !== tax.code) { + const existing = await this.findByCode(tax.tenantId, dto.code); + if (existing) { + throw new ConflictException('Tax code already exists'); + } + } + + Object.assign(tax, dto, { updatedBy: userId }); + return this.repo.save(tax); + } + + async deactivate(id: string): Promise { + const tax = await this.repo.findOneOrFail({ where: { id } }); + tax.active = false; + return this.repo.save(tax); + } + + /** + * Calcula el monto de impuesto para un valor base + */ + calculateTax( + baseAmount: number, + tax: Tax + ): { taxAmount: number; baseAmount: number } { + if (tax.includedInPrice) { + // Tax is included, extract it + const baseWithoutTax = baseAmount / (1 + tax.amount / 100); + return { + baseAmount: baseWithoutTax, + taxAmount: baseAmount - baseWithoutTax, + }; + } else { + // Tax is added on top + return { + baseAmount, + taxAmount: baseAmount * (tax.amount / 100), + }; + } + } + + /** + * Calcula impuestos multiples para un valor base + */ + async calculateTaxes( + baseAmount: number, + taxIds: string[] + ): Promise { + let totalTax = 0; + const taxDetails: TaxDetail[] = []; + + for (const taxId of taxIds) { + const tax = await this.findById(taxId); + const { taxAmount } = this.calculateTax(baseAmount, tax); + + totalTax += taxAmount; + taxDetails.push({ + taxId, + taxName: tax.name, + taxCode: tax.code, + rate: tax.amount, + baseAmount, + taxAmount, + }); + } + + return { + baseAmount, + totalTax, + totalAmount: baseAmount + totalTax, + taxes: taxDetails, + }; + } +} +``` + +### IncotermsService + +```typescript +@Injectable() +export class IncotermsService { + constructor( + @InjectRepository(Incoterm) + private readonly repo: Repository, + ) {} + + async findAll(activeOnly: boolean = true): Promise { + const where = activeOnly ? { isActive: true } : {}; + return this.repo.find({ + where, + order: { code: 'ASC' }, + }); + } + + async findById(id: string): Promise { + return this.repo.findOneOrFail({ where: { id } }); + } + + async findByCode(code: string): Promise { + return this.repo.findOne({ where: { code } }); + } + + async create(dto: CreateIncotermDto): Promise { + const existing = await this.findByCode(dto.code); + if (existing) { + throw new ConflictException('Incoterm code already exists'); + } + + const incoterm = this.repo.create(dto); + return this.repo.save(incoterm); + } + + async update(id: string, dto: UpdateIncotermDto): Promise { + const incoterm = await this.repo.findOneOrFail({ where: { id } }); + + if (dto.code && dto.code !== incoterm.code) { + const existing = await this.findByCode(dto.code); + if (existing) { + throw new ConflictException('Incoterm code already exists'); + } + } + + Object.assign(incoterm, dto); + return this.repo.save(incoterm); + } + + async deactivate(id: string): Promise { + const incoterm = await this.repo.findOneOrFail({ where: { id } }); + incoterm.isActive = false; + return this.repo.save(incoterm); + } + + /** + * Obtiene los Incoterms estandar 2020 + * EXW, FCA, CPT, CIP, DAP, DPU, DDP (any mode) + * FAS, FOB, CFR, CIF (sea/inland waterway) + */ + getStandardIncoterms(): IncotermInfo[] { + return [ + { code: 'EXW', name: 'Ex Works', mode: 'any' }, + { code: 'FCA', name: 'Free Carrier', mode: 'any' }, + { code: 'CPT', name: 'Carriage Paid To', mode: 'any' }, + { code: 'CIP', name: 'Carriage and Insurance Paid To', mode: 'any' }, + { code: 'DAP', name: 'Delivered at Place', mode: 'any' }, + { code: 'DPU', name: 'Delivered at Place Unloaded', mode: 'any' }, + { code: 'DDP', name: 'Delivered Duty Paid', mode: 'any' }, + { code: 'FAS', name: 'Free Alongside Ship', mode: 'sea' }, + { code: 'FOB', name: 'Free on Board', mode: 'sea' }, + { code: 'CFR', name: 'Cost and Freight', mode: 'sea' }, + { code: 'CIF', name: 'Cost, Insurance and Freight', mode: 'sea' }, + ]; + } +} +``` + --- ## Controladores @@ -1082,6 +3246,49 @@ export class AccountsController { | GET | /financial/cost-centers | financial.costcenters.read | List cost centers | | POST | /financial/cost-centers | financial.costcenters.manage | Create cost center | | GET | /financial/reports/trial-balance | financial.reports.read | Trial balance | +| GET | /financial/invoices | financial.invoices.read | List invoices | +| GET | /financial/invoices/:id | financial.invoices.read | Get invoice | +| POST | /financial/invoices | financial.invoices.create | Create invoice | +| PUT | /financial/invoices/:id | financial.invoices.update | Update invoice | +| POST | /financial/invoices/:id/validate | financial.invoices.validate | Validate invoice | +| POST | /financial/invoices/:id/cancel | financial.invoices.cancel | Cancel invoice | +| GET | /financial/payments | financial.payments.read | List payments | +| GET | /financial/payments/:id | financial.payments.read | Get payment | +| POST | /financial/payments | financial.payments.create | Create payment | +| PUT | /financial/payments/:id | financial.payments.update | Update payment | +| POST | /financial/payments/:id/post | financial.payments.post | Post payment | +| POST | /financial/payments/:id/cancel | financial.payments.cancel | Cancel payment | +| POST | /financial/payments/:id/reconcile | financial.payments.reconcile | Reconcile with invoices | +| POST | /financial/payments/:id/auto-reconcile | financial.payments.reconcile | Auto-reconcile payment | +| GET | /financial/payment-methods | financial.paymentmethods.read | List payment methods | +| GET | /financial/payment-methods/:id | financial.paymentmethods.read | Get payment method | +| POST | /financial/payment-methods | financial.paymentmethods.manage | Create payment method | +| PUT | /financial/payment-methods/:id | financial.paymentmethods.manage | Update payment method | +| GET | /financial/payment-terms | financial.paymentterms.read | List payment terms | +| GET | /financial/payment-terms/:id | financial.paymentterms.read | Get payment term | +| POST | /financial/payment-terms | financial.paymentterms.manage | Create payment term | +| PUT | /financial/payment-terms/:id | financial.paymentterms.manage | Update payment term | +| POST | /financial/payment-terms/:id/calculate | financial.paymentterms.read | Calculate due dates | +| GET | /financial/journal-entries | financial.journalentries.read | List journal entries | +| GET | /financial/journal-entries/:id | financial.journalentries.read | Get journal entry | +| POST | /financial/journal-entries | financial.journalentries.create | Create journal entry | +| PUT | /financial/journal-entries/:id | financial.journalentries.update | Update journal entry | +| POST | /financial/journal-entries/:id/post | financial.journalentries.post | Post journal entry | +| POST | /financial/journal-entries/:id/cancel | financial.journalentries.cancel | Cancel journal entry | +| GET | /financial/reconcile-models | financial.reconcilemodels.read | List reconcile models | +| GET | /financial/reconcile-models/:id | financial.reconcilemodels.read | Get reconcile model | +| POST | /financial/reconcile-models | financial.reconcilemodels.manage | Create reconcile model | +| PUT | /financial/reconcile-models/:id | financial.reconcilemodels.manage | Update reconcile model | +| POST | /financial/reconcile-models/:id/apply | financial.reconcilemodels.apply | Apply reconcile model | +| GET | /financial/taxes | financial.taxes.read | List taxes | +| GET | /financial/taxes/:id | financial.taxes.read | Get tax | +| POST | /financial/taxes | financial.taxes.manage | Create tax | +| PUT | /financial/taxes/:id | financial.taxes.manage | Update tax | +| POST | /financial/taxes/calculate | financial.taxes.read | Calculate taxes | +| GET | /financial/incoterms | financial.incoterms.read | List incoterms | +| GET | /financial/incoterms/:id | financial.incoterms.read | Get incoterm | +| POST | /financial/incoterms | financial.incoterms.manage | Create incoterm | +| PUT | /financial/incoterms/:id | financial.incoterms.manage | Update incoterm | --- @@ -1090,3 +3297,4 @@ export class AccountsController { | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | +| 2.0 | 2026-01-10 | Requirements-Analyst | Documentacion de servicios: InvoicesService, PaymentsService (36 tests, reconciliacion), PaymentMethodsService, PaymentTermsService, JournalEntriesService (posting), ReconcileModelsService, TaxesService, IncotermsService | diff --git a/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-001.md b/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-001.md deleted file mode 100644 index b1983fc..0000000 --- a/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-001.md +++ /dev/null @@ -1,306 +0,0 @@ -# US-MGN010-001: Gestionar Plan de Cuentas - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN010-001 | -| **Modulo** | MGN-010 Financial | -| **Sprint** | Sprint 7 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 13 | -| **Estado** | Ready | -| **RF Relacionado** | RF-FIN-001 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** contador o administrador financiero -**Quiero** gestionar el plan de cuentas contables con estructura jerarquica -**Para** organizar la informacion financiera segun normativas contables - ---- - -## Descripcion - -El sistema permite crear y gestionar planes de cuentas con estructura jerarquica ilimitada. Soporta diferentes estandares contables (PCGA, NIIF) e incluye templates predefinidos importables. Los saldos se calculan automaticamente. - -### Contexto - -- Estructura jerarquica con LTREE -- Multiples niveles de detalle -- Templates predefinidos -- Calculo automatico de saldos - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar planes de cuentas - -```gherkin -Given un administrador financiero -When accede a GET /api/v1/financial/charts -Then el sistema responde con status 200 - And retorna planes de cuentas del tenant - And cada plan incluye cantidad de cuentas -``` - -### Escenario 2: Ver arbol de cuentas - -```gherkin -Given un plan de cuentas existente -When consulta GET /api/v1/financial/charts/{id}/accounts?tree=true -Then retorna cuentas en estructura jerarquica - And cada cuenta incluye sus hijos - And muestra nivel en la jerarquia -``` - -### Escenario 3: Crear cuenta de grupo - -```gherkin -Given plan de cuentas activo -When crea cuenta con isDetail = false - | code | "1.1" | - | name | "Activo Circulante" | - | accountType | "asset" | -Then la cuenta se crea como cuenta de grupo - And no permite movimientos directos -``` - -### Escenario 4: Crear cuenta de detalle - -```gherkin -Given cuenta padre "1.1" existente -When crea cuenta con: - | parentId | uuid de 1.1 | - | code | "1.1.01.001" | - | name | "Caja General" | - | isDetail | true | -Then la cuenta se crea como cuenta de detalle - And permite movimientos contables -``` - -### Escenario 5: Validar codigo unico - -```gherkin -Given cuenta "1.1.01.001" existente -When intenta crear otra cuenta con mismo codigo -Then el sistema responde con status 400 - And indica "Codigo de cuenta duplicado" -``` - -### Escenario 6: No eliminar cuenta con movimientos - -```gherkin -Given cuenta con asientos contables registrados -When intenta eliminar la cuenta -Then el sistema responde con status 400 - And indica "No se puede eliminar cuenta con movimientos" -``` - -### Escenario 7: Importar plan desde template - -```gherkin -Given templates disponibles (PCGA_MX, NIIF_BASIC) -When importa POST /api/v1/financial/charts/import - | template | "PCGA_MX" | - | name | "Mi Plan 2025" | -Then se crea plan con cuentas del template - And las cuentas mantienen estructura original -``` - -### Escenario 8: Consultar saldo de cuenta - -```gherkin -Given cuenta con movimientos -When consulta GET /api/v1/financial/accounts/{id}/balance?periodId=uuid -Then retorna: - | openingBalance | 50000 | - | movements.debit | 100000 | - | movements.credit | 80000 | - | closingBalance | 70000 | -``` - -### Escenario 9: Buscar cuentas - -```gherkin -Given multiples cuentas en el plan -When busca GET /api/v1/financial/charts/{id}/accounts?search=banco -Then retorna cuentas cuyo nombre o codigo contiene "banco" -``` - -### Escenario 10: Exportar plan a Excel - -```gherkin -Given plan de cuentas completo -When exporta a Excel -Then genera archivo con columnas: - | codigo, nombre, tipo, naturaleza, nivel, es_detalle | - And mantiene estructura jerarquica visual -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| PLAN DE CUENTAS: PCGA 2025 [+ Nueva Cuenta]| -+------------------------------------------------------------------+ -| [Buscar...] [Expandir todo] [Colapsar todo] [Exportar] | -+------------------------------------------------------------------+ - -| CODIGO | NOMBRE | TIPO | SALDO | -+------------------------------------------------------------------+ -| ▼ 1 | ACTIVO | Activo | $1,200,000 | -| ▼ 1.1 | Activo Circulante | Activo | $500,000 | -| ▼ 1.1.01 | Efectivo y Equivalentes | Activo | $250,000 | -| 1.1.01.001 | Caja General | Activo | $50,000 | -| 1.1.01.002 | Bancos | Activo | $200,000 | -| ▼ 1.1.02 | Cuentas por Cobrar | Activo | $250,000 | -| 1.1.02.001 | Clientes Nacionales | Activo | $180,000 | -| 1.1.02.002 | Clientes Extranjeros | Activo | $70,000 | -| ▼ 1.2 | Activo No Circulante | Activo | $700,000 | -| ... | -| ▼ 2 | PASIVO | Pasivo | $400,000 | -| ... | -| ▼ 3 | CAPITAL | Capital | $800,000 | -| ... | -+------------------------------------------------------------------+ - -Modal: Nueva Cuenta -+------------------------------------------------------------------+ -| NUEVA CUENTA [X] | -+------------------------------------------------------------------+ -| Cuenta Padre | -| [1.1.01 - Efectivo y Equivalentes v] | -| | -| Codigo* | -| [1.1.01.003 ] | -| Formato: X.X.XX.XXX (segun nivel) | -| | -| Nombre* | -| [Fondo Fijo ] | -| | -| Tipo de Cuenta* | -| (•) Activo ( ) Pasivo ( ) Capital ( ) Ingreso ( ) Gasto | -| | -| Naturaleza* | -| (•) Deudora ( ) Acreedora | -| | -| [✓] Cuenta de detalle (permite movimientos) | -| | -| Moneda especifica (opcional) | -| [Usar moneda base v] | -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ - -Modal: Importar Template -+------------------------------------------------------------------+ -| IMPORTAR PLAN DE CUENTAS [X] | -+------------------------------------------------------------------+ -| Template* | -| [PCGA Mexico (500 cuentas) v] | -| | -| Nombre del Plan* | -| [Plan Contable 2025 ] | -| | -| Opciones | -| [ ] Solo importar grupos principales (niveles 1-2) | -| [ ] Incluir descripciones | -| | -| Vista previa: 500 cuentas seran creadas | -| | -| [Cancelar] [=== Importar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /financial/charts | Listar planes | -| POST | /financial/charts | Crear plan | -| GET | /financial/charts/:id | Detalle de plan | -| GET | /financial/charts/:id/accounts | Cuentas del plan | -| POST | /financial/charts/:id/accounts | Crear cuenta | -| PUT | /financial/accounts/:id | Actualizar cuenta | -| DELETE | /financial/accounts/:id | Eliminar cuenta | -| POST | /financial/charts/import | Importar template | -| GET | /financial/accounts/:id/balance | Saldo de cuenta | - -### Estructura LTREE - -```sql --- Columna path para jerarquia -ALTER TABLE accounts ADD COLUMN path LTREE; - --- Indice para consultas jerarquicas -CREATE INDEX idx_accounts_path ON accounts USING GIST (path); - --- Ejemplo de paths: --- 1 -> '1' --- 1.1 -> '1.1' --- 1.1.01 -> '1.1.01' --- 1.1.01.001 -> '1.1.01.001' -``` - -### Tipos de Cuenta - -| Tipo | Naturaleza | Uso | -|------|------------|-----| -| asset | debit | Activos | -| liability | credit | Pasivos | -| equity | credit | Capital | -| income | credit | Ingresos | -| expense | debit | Gastos | -| cost | debit | Costos | - ---- - -## Definicion de Done - -- [ ] CRUD de planes de cuentas -- [ ] Estructura jerarquica con LTREE -- [ ] Templates predefinidos importables -- [ ] Validacion de codigos unicos -- [ ] Calculo automatico de saldos -- [ ] No eliminar cuentas con movimientos -- [ ] Exportar a Excel -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla charts_of_accounts | Planes | -| Tabla accounts | Cuentas con LTREE | -| Extension ltree | Para jerarquias | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN010-004 | Asientos contables | -| Reportes financieros | Balance, Estado Resultados | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-002.md b/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-002.md deleted file mode 100644 index f2fc865..0000000 --- a/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-002.md +++ /dev/null @@ -1,304 +0,0 @@ -# US-MGN010-002: Gestionar Monedas y Tipos de Cambio - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN010-002 | -| **Modulo** | MGN-010 Financial | -| **Sprint** | Sprint 7 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 10 | -| **Estado** | Ready | -| **RF Relacionado** | RF-FIN-002 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador financiero -**Quiero** gestionar multiples monedas y tipos de cambio -**Para** registrar operaciones en diferentes divisas con conversion automatica - ---- - -## Descripcion - -El sistema soporta operaciones multi-moneda con historial de tipos de cambio. Los usuarios pueden registrar tasas manualmente o sincronizar desde APIs externas. Todas las operaciones se convierten a moneda base automaticamente. - -### Contexto - -- Moneda base del tenant -- Historial de tipos de cambio -- Conversion automatica -- Integracion con APIs externas - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar monedas del tenant - -```gherkin -Given un tenant con monedas configuradas -When consulta GET /api/v1/financial/currencies -Then retorna lista de monedas activas - And indica cual es la moneda base - And muestra tipo de cambio actual -``` - -### Escenario 2: Activar moneda - -```gherkin -Given moneda "EUR" disponible pero inactiva -When activa la moneda para el tenant -Then la moneda queda disponible para transacciones - And aparece en selectores de moneda -``` - -### Escenario 3: Registrar tipo de cambio - -```gherkin -Given moneda USD activa -When registra POST /api/v1/financial/currencies/USD/rates - | rate | 17.55 | - | effectiveDate | "2025-12-05" | -Then el tipo de cambio se guarda - And aplica a operaciones desde esa fecha -``` - -### Escenario 4: Consultar historial de tasas - -```gherkin -Given tipos de cambio historicos -When consulta GET /api/v1/financial/currencies/USD/rates?from=2025-11-01&to=2025-12-05 -Then retorna lista de tasas ordenadas por fecha - And incluye source (manual, api) -``` - -### Escenario 5: Convertir monto - -```gherkin -Given tipo de cambio USD/MXN = 17.50 -When envia POST /api/v1/financial/currencies/convert - | amount | 1000 | - | from | "USD" | - | to | "MXN" | - | date | "2025-12-05" | -Then retorna: - | convertedAmount | 17500 | - | exchangeRate | 17.50 | -``` - -### Escenario 6: Tasa no existe para fecha - -```gherkin -Given sin tipo de cambio para fecha especifica -When consulta conversion para esa fecha -Then usa el tipo de cambio mas reciente anterior - And indica que es tasa aproximada -``` - -### Escenario 7: Configurar actualizacion automatica - -```gherkin -Given tenant con API de Banxico configurada -When activa auto-update: - | schedule | "0 8 * * 1-5" | - | currencies | ["USD", "EUR"] | -Then el sistema actualiza tasas automaticamente -``` - -### Escenario 8: Sincronizar desde API externa - -```gherkin -Given integracion con proveedor de tasas -When ejecuta sincronizacion -Then obtiene tasas actuales de la API - And las registra con source = nombre del proveedor -``` - -### Escenario 9: Moneda base no modificable - -```gherkin -Given tenant con movimientos registrados -When intenta cambiar moneda base -Then el sistema responde con error - And indica que hay movimientos existentes -``` - -### Escenario 10: Formato por moneda - -```gherkin -Given monedas con diferentes formatos -When muestra montos -Then aplica formato correcto: - | MXN | $1,234.56 | - | USD | US$1,234.56 | - | EUR | €1.234,56 | - | COP | $1.234 | -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| MONEDAS | -+------------------------------------------------------------------+ -| [+ Activar Moneda] [Sincronizar Tasas] | -+------------------------------------------------------------------+ - -MONEDAS ACTIVAS -+------------------------------------------------------------------+ -| CODIGO | NOMBRE | SIMBOLO | TC ACTUAL | ACCIONES | -+------------------------------------------------------------------+ -| MXN | Peso Mexicano | $ | 1.0000 ⭐ | [Configurar]| -| USD | Dolar Estadounidense| US$ | 17.5000 | [Tasas][⚙] | -| EUR | Euro | € | 19.2500 | [Tasas][⚙] | -+------------------------------------------------------------------+ - -⭐ = Moneda base - -+------------------------------------------------------------------+ -| HISTORIAL DE TASAS - USD [+ Nueva Tasa] | -+------------------------------------------------------------------+ -| [Desde: 2025-11-01] [Hasta: 2025-12-05] [Filtrar] | -+------------------------------------------------------------------+ - -| FECHA | TASA | INVERSA | FUENTE | -+------------------------------------------------------------------+ -| 2025-12-05 | 17.5500 | 0.0570 | Manual | -| 2025-12-04 | 17.5000 | 0.0571 | Banxico | -| 2025-12-03 | 17.4500 | 0.0573 | Banxico | -| 2025-12-02 | 17.5200 | 0.0571 | Banxico | -+------------------------------------------------------------------+ - -[GRAFICO: Tendencia del tipo de cambio] - ^ -18.0| . - | / \ -17.5|.../. \..... - | -17.0+-----------------> - Nov 1 Nov 15 Dic 1 - -Modal: Nueva Tasa de Cambio -+------------------------------------------------------------------+ -| REGISTRAR TIPO DE CAMBIO [X] | -+------------------------------------------------------------------+ -| Moneda: USD - Dolar Estadounidense | -| Moneda Base: MXN - Peso Mexicano | -| | -| Fecha Efectiva* | -| [2025-12-06 ] [📅] | -| | -| Tipo de Cambio* (1 USD = ? MXN) | -| [17.60 ] | -| | -| Tasa Inversa: 0.0568 | -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ - -Modal: Configurar Actualizacion Automatica -+------------------------------------------------------------------+ -| ACTUALIZACION AUTOMATICA DE TASAS [X] | -+------------------------------------------------------------------+ -| [✓] Activar actualizacion automatica | -| | -| Proveedor | -| (•) Banxico (Mexico) | -| ( ) BCE (Europa) | -| ( ) Open Exchange Rates (Global) | -| | -| Programacion | -| Frecuencia: [Diario v] Hora: [08:00 v] | -| Dias: [✓ L] [✓ M] [✓ M] [✓ J] [✓ V] [ S] [ D] | -| | -| Monedas a actualizar | -| [✓] USD [✓] EUR [ ] GBP [ ] JPY [ ] CAD | -| | -| [Cancelar] [=== Guardar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /financial/currencies | Listar monedas | -| POST | /financial/currencies/:code/activate | Activar moneda | -| DELETE | /financial/currencies/:code/deactivate | Desactivar | -| GET | /financial/currencies/:code/rates | Historial tasas | -| POST | /financial/currencies/:code/rates | Registrar tasa | -| POST | /financial/currencies/convert | Convertir monto | -| PUT | /financial/currencies/auto-update | Config auto | -| POST | /financial/currencies/sync | Sincronizar ahora | - -### Proveedores de Tasas - -| Proveedor | Endpoint | Monedas | -|-----------|----------|---------| -| Banxico | api.banxico.org.mx | MXN base | -| BCE | api.exchangeratesapi.io | EUR base | -| Open Exchange | openexchangerates.org | USD base | - -### Precision Decimal - -```typescript -// Usar Decimal.js para precision -import Decimal from 'decimal.js'; - -const rate = new Decimal('17.5543'); -const amount = new Decimal('1000.00'); -const converted = amount.times(rate).toDecimalPlaces(2); -``` - ---- - -## Definicion de Done - -- [ ] CRUD de monedas -- [ ] Historial de tipos de cambio -- [ ] Conversion automatica -- [ ] Integracion con APIs externas -- [ ] Actualizacion programada -- [ ] Formato por moneda -- [ ] Grafico de tendencia -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla currencies | Catalogo base | -| Tabla tenant_currencies | Config por tenant | -| Tabla exchange_rates | Historial | -| Bull Queue | Para sincronizacion | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN010-004 | Asientos multi-moneda | -| Operaciones comerciales | Facturas en USD, etc | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-003.md b/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-003.md deleted file mode 100644 index ba65c25..0000000 --- a/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-003.md +++ /dev/null @@ -1,303 +0,0 @@ -# US-MGN010-003: Gestionar Periodos Contables - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN010-003 | -| **Modulo** | MGN-010 Financial | -| **Sprint** | Sprint 7 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 10 | -| **Estado** | Ready | -| **RF Relacionado** | RF-FIN-003 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** contador -**Quiero** gestionar años fiscales y periodos contables -**Para** controlar el cierre de periodos y restringir movimientos fuera de fechas permitidas - ---- - -## Descripcion - -El sistema gestiona años fiscales con periodos mensuales (u otros). Controla apertura/cierre de periodos, valida fechas de contabilizacion y ejecuta procesos de cierre que generan saldos y asientos de cierre. - -### Contexto - -- Años fiscales con periodos -- Control de fechas de contabilizacion -- Proceso de cierre de periodo -- Cierre anual con asientos automaticos - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar años fiscales - -```gherkin -Given años fiscales existentes -When consulta GET /api/v1/financial/fiscal-years -Then retorna lista de años con: - | name, startDate, endDate, status, openPeriodsCount | -``` - -### Escenario 2: Crear año fiscal - -```gherkin -Given necesidad de nuevo año -When crea POST /api/v1/financial/fiscal-years - | name | "Ejercicio 2026" | - | startDate | "2026-01-01" | - | endDate | "2026-12-31" | - | periodType | "monthly" | -Then se crea el año con 12 periodos automaticamente - And los periodos tienen status = "future" -``` - -### Escenario 3: Ver periodos del año - -```gherkin -Given año fiscal 2025 -When consulta GET /api/v1/financial/fiscal-years/{id}/periods -Then retorna lista de 12 periodos: - | Enero: closed, Febrero: closed, ..., Diciembre: open | -``` - -### Escenario 4: Abrir periodo - -```gherkin -Given periodo con status = "future" -When ejecuta POST /api/v1/financial/periods/{id}/open -Then el periodo cambia a status = "open" - And permite movimientos contables -``` - -### Escenario 5: Validar fecha de contabilizacion - -```gherkin -Given periodo Noviembre cerrado -When intenta registrar asiento con fecha en Noviembre -Then el sistema rechaza el movimiento - And indica "Periodo cerrado para contabilizacion" -``` - -### Escenario 6: Cerrar periodo - -```gherkin -Given periodo abierto con movimientos cuadrados -When ejecuta POST /api/v1/financial/periods/{id}/close -Then el sistema: - | Valida que saldos cuadran | - | Calcula saldos de cierre | - | Cambia status a "closed" | -``` - -### Escenario 7: Cerrar periodo con advertencias - -```gherkin -Given periodo con documentos pendientes -When intenta cerrar -Then el sistema muestra advertencias: - | "5 facturas sin contabilizar" | - And permite continuar o cancelar -``` - -### Escenario 8: Reabrir periodo (excepcion) - -```gherkin -Given periodo cerrado - And usuario con permiso especial -When ejecuta POST /api/v1/financial/periods/{id}/reopen - | reason | "Correccion de asiento" | -Then el periodo se reabre temporalmente - And se registra en audit log -``` - -### Escenario 9: Cierre de año fiscal - -```gherkin -Given todos los periodos del año cerrados -When ejecuta POST /api/v1/financial/fiscal-years/{id}/close -Then el sistema: - | Genera asiento de cierre (P&L -> Utilidades) | - | Calcula saldos iniciales para nuevo año | - | Crea asiento de apertura en nuevo año | - | Marca año como "closed" | -``` - -### Escenario 10: Periodo de ajuste - -```gherkin -Given año fiscal con periodo de ajuste habilitado -When consulta periodos -Then existe periodo 13 "Ajustes" - And solo permite asientos de ajuste -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| AÑOS FISCALES [+ Nuevo Año] | -+------------------------------------------------------------------+ - -| AÑO | INICIO | FIN | PERIODOS | ESTADO | -+------------------------------------------------------------------+ -| Ejercicio 2025| 2025-01-01 | 2025-12-31 | 11/12 | ✓ Abierto | -| Ejercicio 2024| 2024-01-01 | 2024-12-31 | 12/12 | 🔒 Cerrado | -| Ejercicio 2023| 2023-01-01 | 2023-12-31 | 12/12 | 🔒 Cerrado | -+------------------------------------------------------------------+ - -+------------------------------------------------------------------+ -| PERIODOS - EJERCICIO 2025 | -+------------------------------------------------------------------+ - -| # | PERIODO | INICIO | FIN | ESTADO | ACCIONES | -+------------------------------------------------------------------+ -| 1 | Enero 2025 | 2025-01-01 | 2025-01-31 | 🔒 Cerrado | | -| 2 | Febrero 2025 | 2025-02-01 | 2025-02-28 | 🔒 Cerrado | | -| 3 | Marzo 2025 | 2025-03-01 | 2025-03-31 | 🔒 Cerrado | | -| 4 | Abril 2025 | 2025-04-01 | 2025-04-30 | 🔒 Cerrado | | -| 5 | Mayo 2025 | 2025-05-01 | 2025-05-31 | 🔒 Cerrado | | -| 6 | Junio 2025 | 2025-06-01 | 2025-06-30 | 🔒 Cerrado | | -| 7 | Julio 2025 | 2025-07-01 | 2025-07-31 | 🔒 Cerrado | | -| 8 | Agosto 2025 | 2025-08-01 | 2025-08-31 | 🔒 Cerrado | | -| 9 | Septiembre 2025| 2025-09-01| 2025-09-30 | 🔒 Cerrado | | -| 10| Octubre 2025 | 2025-10-01 | 2025-10-31 | 🔒 Cerrado | | -| 11| Noviembre 2025| 2025-11-01 | 2025-11-30 | 🔒 Cerrado |[Reabrir] | -| 12| Diciembre 2025| 2025-12-01 | 2025-12-31 | ✓ Abierto | [Cerrar] | -+------------------------------------------------------------------+ - -Modal: Cerrar Periodo -+------------------------------------------------------------------+ -| CERRAR PERIODO: Diciembre 2025 [X] | -+------------------------------------------------------------------+ -| Validaciones previas al cierre: | -| | -| [✓] Todos los asientos cuadrados | -| [✓] Saldos de cuentas verificados | -| [⚠] 3 facturas pendientes de contabilizar (no obligatorio) | -| | -| Resumen de periodo: | -| - Total asientos: 245 | -| - Total debe: $5,234,567.00 | -| - Total haber: $5,234,567.00 | -| - Diferencia: $0.00 | -| | -| [✓] Generar saldos de cierre | -| [✓] Notificar a contadores | -| | -| [Cancelar] [=== Cerrar Periodo ===] | -+------------------------------------------------------------------+ - -Modal: Cierre de Año -+------------------------------------------------------------------+ -| CIERRE DEL EJERCICIO 2025 [X] | -+------------------------------------------------------------------+ -| Este proceso realizara: | -| | -| 1. Cierre de cuentas de resultados | -| - Ingresos -> $8,500,000 | -| - Gastos -> $6,200,000 | -| - Resultado del ejercicio -> $2,300,000 | -| | -| 2. Traspaso a utilidades acumuladas | -| - Cuenta: 3.4.01 - Utilidades Acumuladas | -| | -| 3. Generacion de saldos iniciales 2026 | -| - Se crearan 156 saldos iniciales | -| | -| [✓] Crear nuevo ejercicio 2026 automaticamente | -| | -| ⚠️ Este proceso no puede revertirse | -| | -| [Cancelar] [=== Cerrar Ejercicio ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /financial/fiscal-years | Listar años | -| POST | /financial/fiscal-years | Crear año | -| GET | /financial/fiscal-years/:id | Detalle de año | -| GET | /financial/fiscal-years/:id/periods | Periodos | -| POST | /financial/periods/:id/open | Abrir periodo | -| POST | /financial/periods/:id/close | Cerrar periodo | -| POST | /financial/periods/:id/reopen | Reabrir | -| POST | /financial/fiscal-years/:id/close | Cierre anual | - -### Proceso de Cierre - -```typescript -async function closePeriod(periodId: UUID): Promise { - await this.validateBalances(periodId); - await this.checkPendingDocs(periodId); - await this.calculateClosingBalances(periodId); - await this.updatePeriodStatus(periodId, 'closed'); - await this.notifyAccountants(periodId); - return { success: true }; -} -``` - -### Validaciones - -| Validacion | Obligatoria | Descripcion | -|------------|-------------|-------------| -| Saldos cuadrados | Si | Debe = Haber | -| Docs pendientes | No | Advertencia | -| Asientos en borrador | No | Advertencia | - ---- - -## Definicion de Done - -- [ ] CRUD de años fiscales -- [ ] Generacion automatica de periodos -- [ ] Control de fechas de contabilizacion -- [ ] Proceso de cierre de periodo -- [ ] Proceso de cierre de año -- [ ] Generacion de saldos iniciales -- [ ] Reabrir con autorizacion -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla fiscal_years | Años fiscales | -| Tabla fiscal_periods | Periodos | -| US-MGN010-001 | Plan de cuentas | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN010-004 | Asientos (valida periodo) | -| Reportes | Balance, Estado Resultados | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-004.md b/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-004.md deleted file mode 100644 index 426cb98..0000000 --- a/docs/02-fase-core-business/MGN-010-financial/historias-usuario/US-MGN010-004.md +++ /dev/null @@ -1,311 +0,0 @@ -# US-MGN010-004: Registrar Asientos Contables - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN010-004 | -| **Modulo** | MGN-010 Financial | -| **Sprint** | Sprint 7 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 13 | -| **Estado** | Ready | -| **RF Relacionado** | RF-FIN-004 | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** contador -**Quiero** registrar asientos contables con multiples lineas -**Para** afectar los saldos de cuentas y mantener la contabilidad al dia - ---- - -## Descripcion - -El sistema permite registrar asientos contables (journal entries) con multiples lineas de cargo y abono. Valida que el asiento este cuadrado, la fecha en periodo abierto y las cuentas sean de detalle. Soporta multi-moneda y centros de costo. - -### Contexto - -- Partida doble (Debe = Haber) -- Validacion de periodo abierto -- Numeracion automatica -- Mayorizar (post) afecta saldos - ---- - -## Criterios de Aceptacion - -### Escenario 1: Crear asiento en borrador - -```gherkin -Given usuario con permiso contable -When crea POST /api/v1/financial/journal - | entryDate | "2025-12-05" | - | description | "Registro de venta" | - | lines | [{ accountId, debit/credit }, ...] | -Then el asiento se crea con status = "draft" - And aun no afecta saldos de cuentas -``` - -### Escenario 2: Validar asiento cuadrado - -```gherkin -Given asiento con total Debe = 11,600 - And total Haber = 10,000 -When intenta contabilizar -Then el sistema rechaza con error - And indica "Asiento descuadrado: Debe=11,600, Haber=10,000" -``` - -### Escenario 3: Contabilizar asiento (post) - -```gherkin -Given asiento en borrador cuadrado -When ejecuta POST /api/v1/financial/journal/{id}/post -Then el asiento cambia a status = "posted" - And se genera numero automatico - And se actualizan saldos de cuentas afectadas -``` - -### Escenario 4: Numeracion automatica - -```gherkin -Given configuracion de numeracion: - | prefix | "POL" | - | yearFormat | "YYYY" | - | sequenceLength | 6 | -When contabiliza asiento -Then recibe numero "POL-2025-000001" -``` - -### Escenario 5: Fecha en periodo cerrado - -```gherkin -Given periodo Noviembre cerrado -When crea asiento con fecha "2025-11-15" -Then el sistema rechaza - And indica "Periodo cerrado para esta fecha" -``` - -### Escenario 6: Asiento multi-moneda - -```gherkin -Given asiento con linea en USD -When registra la linea: - | amount | 1000 USD | - | exchangeRate | 17.50 | -Then se guarda: - | debit | 1000 (moneda original) | - | debitBase | 17500 (moneda base MXN) | -``` - -### Escenario 7: Reversar asiento - -```gherkin -Given asiento contabilizado -When ejecuta POST /api/v1/financial/journal/{id}/reverse - | reversalDate | "2025-12-06" | - | reason | "Error en monto" | -Then se crea nuevo asiento con lineas invertidas - And el original cambia a status = "reversed" - And los saldos vuelven a estado anterior -``` - -### Escenario 8: Asiento con centro de costo - -```gherkin -Given asiento de gasto -When registra linea con: - | accountId | Gastos Administrativos | - | costCenterId | Centro Norte | -Then el movimiento queda asociado al centro de costo - And reportes por CC muestran el monto -``` - -### Escenario 9: Consultar libro diario - -```gherkin -Given asientos contabilizados -When consulta GET /api/v1/financial/journal?periodId=uuid -Then retorna lista de asientos: - | entryNumber, entryDate, description, totalDebit, linesCount | -``` - -### Escenario 10: Ver detalle de asiento - -```gherkin -Given asiento existente -When consulta GET /api/v1/financial/journal/{id} -Then retorna asiento con todas sus lineas: - | lineNumber, account, debit, credit, description | -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| LIBRO DIARIO [+ Nuevo Asiento] | -+------------------------------------------------------------------+ -| [Periodo: Diciembre 2025 v] [Estado: Todos v] [Buscar...] | -+------------------------------------------------------------------+ - -| NUMERO | FECHA | CONCEPTO | DEBE | LINEAS | -+------------------------------------------------------------------+ -| POL-2025-001245| 2025-12-05 | Pago a proveedor XYZ | $10,000 | 3 | -| POL-2025-001244| 2025-12-05 | Registro de venta | $11,600 | 3 | -| POL-2025-001243| 2025-12-04 | Nomina quincenal | $85,000 | 15 | -| POL-2025-001242| 2025-12-04 | Compra de inventario | $45,000 | 2 | -+------------------------------------------------------------------+ - [< Anterior] [1] [Siguiente >] - -+------------------------------------------------------------------+ -| NUEVO ASIENTO CONTABLE | -+------------------------------------------------------------------+ -| Fecha* | Concepto/Glosa* | -| [2025-12-05] [📅] | [Registro de venta #1234 ] | -+------------------------------------------------------------------+ -| Referencia | Fuente | -| [FAC-2025-1234 ] | [Factura v] | -+------------------------------------------------------------------+ - -LINEAS -+------------------------------------------------------------------+ -| # | CUENTA | DESCRIPCION | DEBE | HABER | -+------------------------------------------------------------------+ -| 1 | [1.1.02.001 Clien | [Cliente ABC ] | [11,600] | [ ] | -| 2 | [4.1.01.001 Ingre | [Venta servici ] | [ ] | [10,000 ] | -| 3 | [2.1.05.001 IVA x | [IVA 16% ] | [ ] | [1,600 ] | -+------------------------------------------------------------------+ - [+ Agregar Linea] -+------------------------------------------------------------------+ -| TOTALES | $11,600 | $11,600 | -| DIFERENCIA | $0.00 ✓ | -+------------------------------------------------------------------+ - - [Cancelar] [Guardar Borrador] [=== Contabilizar ===] - -Modal: Detalle de Asiento -+------------------------------------------------------------------+ -| ASIENTO: POL-2025-001244 [X] | -+------------------------------------------------------------------+ -| Numero: POL-2025-001244 | -| Fecha: 05 de Diciembre de 2025 | -| Concepto: Registro de venta #1234 | -| Estado: ✓ Contabilizado | -| Contabilizado por: Juan Contador - 2025-12-05 10:30 | -| | -| LINEAS | -+------------------------------------------------------------------+ -| CUENTA | DESCRIPCION | DEBE | HABER | -+------------------------------------------------------------------+ -| 1.1.02.001 Clientes Nac. | Cliente ABC | $11,600 | | -| 4.1.01.001 Ingresos Venta | Venta servic. | | $10,000 | -| 2.1.05.001 IVA por Pagar | IVA 16% | | $1,600 | -+------------------------------------------------------------------+ -| TOTALES | $11,600 | $11,600 | -+------------------------------------------------------------------+ -| | -| [Imprimir] [=== Reversar ===] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -| Metodo | Path | Descripcion | -|--------|------|-------------| -| GET | /financial/journal | Listar asientos | -| POST | /financial/journal | Crear asiento | -| GET | /financial/journal/:id | Detalle de asiento | -| PUT | /financial/journal/:id | Actualizar borrador | -| DELETE | /financial/journal/:id | Eliminar borrador | -| POST | /financial/journal/:id/post | Contabilizar | -| POST | /financial/journal/:id/reverse | Reversar | - -### Validaciones - -| Validacion | Descripcion | -|------------|-------------| -| entry_balanced | Debe = Haber (tolerancia 0.01) | -| date_in_open_period | Fecha en periodo abierto | -| accounts_are_detail | Solo cuentas de detalle | -| accounts_active | Cuentas activas | -| amounts_positive | Montos positivos | -| valid_exchange_rate | TC valido para fecha | - -### Mayorizar - -```typescript -async function postEntry(entryId: UUID): Promise { - const entry = await this.getEntry(entryId); - - // Validar - const validation = this.validate(entry); - if (!validation.isValid) throw new ValidationError(validation.errors); - - // Generar numero - const entryNumber = await this.generateNumber(entry.entryDate); - - // Actualizar saldos (en transaccion) - await this.db.transaction(async (tx) => { - for (const line of entry.lines) { - await this.updateAccountBalance(tx, line); - } - await this.markAsPosted(tx, entry, entryNumber); - }); - - return { entryNumber, postedAt: new Date() }; -} -``` - ---- - -## Definicion de Done - -- [ ] CRUD de asientos -- [ ] Validacion de cuadre -- [ ] Validacion de periodo abierto -- [ ] Numeracion automatica -- [ ] Mayorizar asiento -- [ ] Reversar asiento -- [ ] Soporte multi-moneda -- [ ] Centros de costo -- [ ] Tests unitarios -- [ ] Documentacion Swagger - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN010-001 | Plan de cuentas | -| US-MGN010-002 | Monedas y TC | -| US-MGN010-003 | Periodos (validacion) | -| Tabla journal_entries | Asientos | -| Tabla journal_lines | Lineas | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Reportes contables | Balance, Libro Mayor | -| Modulos de negocio | Generan asientos | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial | diff --git a/docs/03-fase-mobile/MOB-001-foundation/_MAP.md b/docs/03-fase-mobile/MOB-001-foundation/_MAP.md new file mode 100644 index 0000000..e05b465 --- /dev/null +++ b/docs/03-fase-mobile/MOB-001-foundation/_MAP.md @@ -0,0 +1,272 @@ +# _MAP: MOB-001 - Mobile Foundation + +**Modulo:** MOB-001 +**Nombre:** Mobile App Foundation +**Fase:** 03 - Mobile +**Story Points:** 31 SP (MOB-001: 13 + MOB-002: 8 + MOB-003: 5 + TEST-010: 5) +**Estado:** COMPLETADO +**Ultima actualizacion:** 2026-01-07 + +--- + +## Resumen + +Aplicacion movil para ERP Core construida con Expo/React Native. Incluye autenticacion, navegacion, funcionalidades offline, notificaciones push, escaneo de codigos de barras/QR y autenticacion biometrica. + +--- + +## Metricas + +| Metrica | Valor | +|---------|-------| +| Story Points | 31 SP | +| Archivos creados | 30+ | +| Screens | 6 (Home, Partners, Scanner, Products, Invoices, Settings) | +| Auth Screens | 2 (Login, Forgot Password) | +| Services | 5 (api, offline, notifications, barcode, biometrics) | +| Hooks | 4 (useOfflineQuery, useNotifications, useBarcode, useBiometrics) | +| Components | 1 (BarcodeScanner) | +| Tests | 57+ | + +--- + +## Estructura del Modulo + +``` +mobile/ +├── app/ # Expo Router screens +│ ├── _layout.tsx # Root layout con auth flow +│ ├── (auth)/ # Auth screens +│ │ ├── _layout.tsx # Auth stack layout +│ │ ├── login.tsx # Login screen +│ │ └── forgot-password.tsx +│ └── (tabs)/ # Main tab screens +│ ├── _layout.tsx # Tab navigation (6 tabs) +│ ├── index.tsx # Home/Dashboard +│ ├── partners.tsx # Partners list +│ ├── scanner.tsx # Barcode scanner +│ ├── products.tsx # Products list +│ ├── invoices.tsx # Invoices list +│ └── settings.tsx # User settings +├── src/ +│ ├── __tests__/ # Unit tests +│ │ ├── auth.store.test.ts +│ │ ├── offline.service.test.ts +│ │ └── biometrics.service.test.ts +│ ├── components/ # Shared components +│ │ ├── index.ts +│ │ └── BarcodeScanner.tsx +│ ├── hooks/ # Custom hooks +│ │ ├── index.ts +│ │ ├── useOfflineQuery.ts +│ │ ├── useNotifications.ts +│ │ ├── useBarcode.ts +│ │ └── useBiometrics.ts +│ ├── services/ # API y servicios +│ │ ├── index.ts +│ │ ├── api.ts # Axios client +│ │ ├── offline.ts # Offline sync +│ │ ├── notifications.ts # Push notifications +│ │ ├── barcode.ts # Barcode scanning +│ │ └── biometrics.ts # Face ID/Touch ID +│ ├── stores/ # Zustand stores +│ │ ├── index.ts +│ │ └── auth.store.ts +│ └── types/ # TypeScript types +│ └── index.ts +├── jest.config.js # Jest configuration +├── jest.setup.js # Test setup with mocks +├── package.json +├── app.json +└── tsconfig.json +``` + +--- + +## Tareas Completadas + +### MOB-001: Foundation (13 SP) + +| Componente | Descripcion | Estado | +|------------|-------------|--------| +| Expo Setup | package.json, app.json, tsconfig.json | Completado | +| Auth Store | Zustand store para autenticacion | Completado | +| API Client | Axios con interceptors y auto-refresh | Completado | +| Auth Screens | Login, Forgot Password | Completado | +| Tab Screens | Home, Partners, Products, Invoices, Settings | Completado | +| Navigation | Expo Router file-based | Completado | +| Types | User, Partner, Product, Invoice, etc. | Completado | + +### MOB-002: Extended Features (8 SP) + +| Feature | Descripcion | Estado | +|---------|-------------|--------| +| Offline Sync | AsyncStorage cache, NetInfo, sync queue | Completado | +| Push Notifications | expo-notifications, Android channels | Completado | +| Camera/QR Scanner | expo-camera, EAN/UPC validation | Completado | +| Biometrics | Face ID/Touch ID/Fingerprint | Completado | + +### MOB-003: Scanner Screen (5 SP) + +| Componente | Descripcion | Estado | +|------------|-------------|--------| +| Scanner Screen | Pantalla dedicada para escaneo | Completado | +| Product Lookup | Busqueda de producto por codigo | Completado | +| Scan History | Historial de ultimos 20 escaneos | Completado | +| Actions | Agregar a inventario/pedido | Completado | + +### TEST-010: Mobile Unit Tests (5 SP) + +| Test File | Tests | Cobertura | +|-----------|------:|-----------| +| auth.store.test.ts | 12 | login, logout, loadStoredAuth, setUser | +| offline.service.test.ts | 25+ | store, cache, network monitor, sync manager | +| biometrics.service.test.ts | 20+ | capabilities, authenticate, enable/disable | +| **TOTAL** | **57+** | | + +--- + +## Dependencias Expo + +| Paquete | Version | Uso | +|---------|---------|-----| +| expo | ~51.0.0 | Framework base | +| expo-router | ~3.5.0 | File-based navigation | +| expo-secure-store | ~13.0.1 | Token storage | +| expo-camera | ~15.0.14 | Barcode scanning | +| expo-barcode-scanner | ~13.0.1 | Barcode types | +| expo-notifications | ~0.28.16 | Push notifications | +| expo-local-authentication | ~14.0.1 | Biometrics | +| expo-haptics | ~13.0.1 | Haptic feedback | +| @react-native-async-storage/async-storage | 1.23.1 | Offline cache | +| @react-native-community/netinfo | 11.3.1 | Network monitoring | +| zustand | ^5.0.1 | State management | +| axios | ^1.7.7 | HTTP client | +| zod | ^3.23.8 | Validation | + +--- + +## Dependencias de Otros Modulos + +**Depende de:** +- MGN-001 (Auth) - Endpoints de autenticacion +- Backend API - Todos los endpoints REST + +**Requerido por:** +- Usuarios moviles del ERP + +--- + +## API Endpoints Consumidos + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| POST | /auth/login | Inicio de sesion | +| POST | /auth/logout | Cierre de sesion | +| GET | /auth/profile | Perfil del usuario | +| POST | /auth/forgot-password | Recuperacion de password | +| GET | /partners | Lista de partners | +| GET | /products | Lista de productos | +| GET | /products?barcode={code} | Buscar por codigo | +| GET | /invoices | Lista de facturas | + +--- + +## Features Detalladas + +### Offline Sync + +```typescript +// Store para estado offline +useOfflineStore: { + isOnline: boolean; + syncQueue: SyncAction[]; + isSyncing: boolean; + lastSyncAt: number | null; +} + +// Hooks disponibles +useOfflineQuery(key, fetcher, options) // Fetch con cache +useOfflineMutation(mutator, options) // Mutacion offline-capable +``` + +### Push Notifications + +```typescript +// Android channels configurados +- default: Notificaciones generales +- alerts: Alertas importantes (high priority) +- sync: Sincronizacion (low priority) + +// Hooks disponibles +useNotifications() // Gestion de notificaciones +``` + +### Barcode Scanner + +```typescript +// Formatos soportados +- EAN-13, EAN-8, UPC-A, UPC-E +- Code128, Code39 +- QR Code + +// Hooks disponibles +useBarcode() // Escaneo y validacion +``` + +### Biometrics + +```typescript +// Tipos soportados +- Face ID (iOS) +- Touch ID (iOS) +- Fingerprint (Android) +- Iris (Android) + +// Hooks disponibles +useBiometrics() // Autenticacion biometrica +``` + +--- + +## Testing + +### Jest Configuration + +```javascript +// jest.config.js +module.exports = { + preset: 'jest-expo', + setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, +}; +``` + +### Mocked Modules + +- expo-secure-store +- @react-native-async-storage/async-storage +- @react-native-community/netinfo +- expo-notifications +- expo-local-authentication +- expo-camera +- expo-haptics +- expo-device + +--- + +## Proximos Pasos + +| ID | Descripcion | SP | Prioridad | +|----|-------------|----|-----------| +| MOB-004 | Detox E2E Tests | 5 | Baja | +| MOB-005 | Orders/Sales Module | 8 | Baja | +| MOB-006 | Offline-first CRUD | 5 | Baja | + +--- + +**Actualizado por:** Mobile-Agent (Claude Opus 4.5) +**Fecha:** 2026-01-07 +**Tareas:** MOB-001, MOB-002, MOB-003, TEST-010 COMPLETADAS diff --git a/docs/03-fase-vertical/MGN-011-sales/README.md b/docs/03-fase-vertical/MGN-011-sales/README.md new file mode 100644 index 0000000..dbbce03 --- /dev/null +++ b/docs/03-fase-vertical/MGN-011-sales/README.md @@ -0,0 +1,47 @@ +# MGN-011: Sales (Gestión de Ventas) + +## Descripción + +Módulo de gestión completa del ciclo de ventas, incluyendo cotizaciones, órdenes de venta, listas de precios y equipos comerciales. + +## Implementación Backend + +**Ubicación:** `/backend/src/modules/sales/` + +### Servicios Implementados + +| Servicio | Descripción | +|----------|-------------| +| `pricelists.service.ts` | Gestión de listas de precios | +| `sales-teams.service.ts` | Equipos comerciales | +| `customer-groups.service.ts` | Grupos de clientes | +| `quotations.service.ts` | Cotizaciones | +| `orders.service.ts` | Órdenes de venta | + +### Estados de Documentos + +- `draft` - Borrador +- `sent` - Enviado +- `confirmed` - Confirmado +- `cancelled` - Cancelado +- `expired` - Expirado + +## Dependencias + +- **MGN-017 (Partners):** Clientes +- **MGN-013 (Inventory):** Productos +- **MGN-010 (Financial):** Facturación, impuestos +- **MGN-005 (Catalogs):** Monedas, UOM + +## Estado de Documentación + +| Artefacto | Estado | +|-----------|--------| +| README | Básico | +| Requerimientos Funcionales | Pendiente | +| Especificaciones Técnicas | Pendiente | +| User Stories | Pendiente | + +--- + +*Módulo identificado durante sincronización docs-código: 2026-01-10* diff --git a/docs/03-fase-vertical/MGN-011-sales/especificaciones/ET-SALES-BACKEND.md b/docs/03-fase-vertical/MGN-011-sales/especificaciones/ET-SALES-BACKEND.md new file mode 100644 index 0000000..846b893 --- /dev/null +++ b/docs/03-fase-vertical/MGN-011-sales/especificaciones/ET-SALES-BACKEND.md @@ -0,0 +1,723 @@ +# ET-SALES-BACKEND +# Especificacion Tecnica Backend - Modulo Ventas + +--- + +## METADATOS + +| Campo | Valor | +|-------|-------| +| **Modulo** | MGN-011 | +| **Version** | 1.0.0 | +| **Fecha** | 2026-01-10 | +| **Estado** | Documentado | +| **Autor** | Claude Code | + +--- + +## 1. SERVICIOS + +### 1.1 CustomerGroupsService + +**Ubicacion:** `backend/src/modules/sales/customer-groups.service.ts` + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string`, `filters: CustomerGroupFilters` | `Promise<{ data: CustomerGroup[]; total: number }>` | Lista grupos de clientes con paginacion y busqueda | +| `findById` | `id: string`, `tenantId: string` | `Promise` | Obtiene grupo por ID con sus miembros | +| `create` | `dto: CreateCustomerGroupDto`, `tenantId: string`, `userId: string` | `Promise` | Crea nuevo grupo de clientes | +| `update` | `id: string`, `dto: UpdateCustomerGroupDto`, `tenantId: string` | `Promise` | Actualiza grupo existente | +| `delete` | `id: string`, `tenantId: string` | `Promise` | Elimina grupo (solo si no tiene miembros) | +| `addMember` | `groupId: string`, `partnerId: string`, `tenantId: string` | `Promise` | Agrega cliente al grupo | +| `removeMember` | `groupId: string`, `memberId: string`, `tenantId: string` | `Promise` | Elimina cliente del grupo | + +--- + +### 1.2 SalesTeamsService + +**Ubicacion:** `backend/src/modules/sales/sales-teams.service.ts` + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string`, `filters: SalesTeamFilters` | `Promise<{ data: SalesTeam[]; total: number }>` | Lista equipos de ventas con filtros | +| `findById` | `id: string`, `tenantId: string` | `Promise` | Obtiene equipo por ID con miembros | +| `create` | `dto: CreateSalesTeamDto`, `tenantId: string`, `userId: string` | `Promise` | Crea nuevo equipo de ventas | +| `update` | `id: string`, `dto: UpdateSalesTeamDto`, `tenantId: string`, `userId: string` | `Promise` | Actualiza equipo existente | +| `addMember` | `teamId: string`, `userId: string`, `role: string`, `tenantId: string` | `Promise` | Agrega usuario al equipo | +| `removeMember` | `teamId: string`, `memberId: string`, `tenantId: string` | `Promise` | Elimina usuario del equipo | + +--- + +### 1.3 PricelistsService + +**Ubicacion:** `backend/src/modules/sales/pricelists.service.ts` + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string`, `filters: PricelistFilters` | `Promise<{ data: Pricelist[]; total: number }>` | Lista listas de precios | +| `findById` | `id: string`, `tenantId: string` | `Promise` | Obtiene lista de precios con items | +| `create` | `dto: CreatePricelistDto`, `tenantId: string`, `userId: string` | `Promise` | Crea nueva lista de precios | +| `update` | `id: string`, `dto: UpdatePricelistDto`, `tenantId: string`, `userId: string` | `Promise` | Actualiza lista de precios | +| `addItem` | `pricelistId: string`, `dto: CreatePricelistItemDto`, `tenantId: string`, `userId: string` | `Promise` | Agrega item a lista de precios | +| `removeItem` | `pricelistId: string`, `itemId: string`, `tenantId: string` | `Promise` | Elimina item de lista de precios | +| `getProductPrice` | `productId: string`, `pricelistId: string`, `quantity: number` | `Promise` | Obtiene precio de producto segun lista y cantidad | + +--- + +### 1.4 OrdersService + +**Ubicacion:** `backend/src/modules/sales/orders.service.ts` + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string`, `filters: SalesOrderFilters` | `Promise<{ data: SalesOrder[]; total: number }>` | Lista ordenes de venta con filtros avanzados | +| `findById` | `id: string`, `tenantId: string` | `Promise` | Obtiene orden por ID con lineas | +| `create` | `dto: CreateSalesOrderDto`, `tenantId: string`, `userId: string` | `Promise` | Crea nueva orden de venta | +| `update` | `id: string`, `dto: UpdateSalesOrderDto`, `tenantId: string`, `userId: string` | `Promise` | Actualiza orden (solo en estado draft) | +| `delete` | `id: string`, `tenantId: string` | `Promise` | Elimina orden (solo en estado draft) | +| `addLine` | `orderId: string`, `dto: CreateSalesOrderLineDto`, `tenantId: string`, `userId: string` | `Promise` | Agrega linea a orden | +| `updateLine` | `orderId: string`, `lineId: string`, `dto: UpdateSalesOrderLineDto`, `tenantId: string` | `Promise` | Actualiza linea de orden | +| `removeLine` | `orderId: string`, `lineId: string`, `tenantId: string` | `Promise` | Elimina linea de orden | +| `confirm` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Confirma orden (draft -> sent) | +| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Cancela orden | +| `createInvoice` | `id: string`, `tenantId: string`, `userId: string` | `Promise<{ orderId: string; invoiceId: string }>` | Genera factura desde orden | + +--- + +### 1.5 QuotationsService + +**Ubicacion:** `backend/src/modules/sales/quotations.service.ts` + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string`, `filters: QuotationFilters` | `Promise<{ data: Quotation[]; total: number }>` | Lista cotizaciones con filtros | +| `findById` | `id: string`, `tenantId: string` | `Promise` | Obtiene cotizacion por ID con lineas | +| `create` | `dto: CreateQuotationDto`, `tenantId: string`, `userId: string` | `Promise` | Crea nueva cotizacion | +| `update` | `id: string`, `dto: UpdateQuotationDto`, `tenantId: string`, `userId: string` | `Promise` | Actualiza cotizacion (solo en draft) | +| `delete` | `id: string`, `tenantId: string` | `Promise` | Elimina cotizacion (solo en draft) | +| `addLine` | `quotationId: string`, `dto: CreateQuotationLineDto`, `tenantId: string`, `userId: string` | `Promise` | Agrega linea a cotizacion | +| `updateLine` | `quotationId: string`, `lineId: string`, `dto: UpdateQuotationLineDto`, `tenantId: string` | `Promise` | Actualiza linea de cotizacion | +| `removeLine` | `quotationId: string`, `lineId: string`, `tenantId: string` | `Promise` | Elimina linea de cotizacion | +| `send` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Envia cotizacion al cliente (email) | +| `confirm` | `id: string`, `tenantId: string`, `userId: string` | `Promise<{ quotation: Quotation; orderId: string }>` | Confirma cotizacion y crea orden de venta | +| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Cancela cotizacion | + +--- + +## 2. ENTIDADES + +### 2.1 CustomerGroup + +**Schema:** `sales.customer_groups` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `tenant_id` | UUID | NO | Tenant (multi-tenancy) | +| `name` | VARCHAR(255) | NO | Nombre del grupo | +| `description` | TEXT | SI | Descripcion | +| `discount_percentage` | DECIMAL | NO | Porcentaje de descuento (default: 0) | +| `created_by` | UUID | SI | Usuario que creo | +| `created_at` | TIMESTAMP | NO | Fecha de creacion | + +### 2.2 CustomerGroupMember + +**Schema:** `sales.customer_group_members` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `customer_group_id` | UUID | NO | FK a customer_groups | +| `partner_id` | UUID | NO | FK a core.partners | +| `joined_at` | TIMESTAMP | NO | Fecha de union | + +### 2.3 SalesTeam + +**Schema:** `sales.sales_teams` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `tenant_id` | UUID | NO | Tenant | +| `company_id` | UUID | NO | FK a auth.companies | +| `name` | VARCHAR(255) | NO | Nombre del equipo | +| `code` | VARCHAR(50) | SI | Codigo unico por empresa | +| `team_leader_id` | UUID | SI | FK a auth.users (lider) | +| `target_monthly` | DECIMAL | SI | Meta mensual | +| `target_annual` | DECIMAL | SI | Meta anual | +| `active` | BOOLEAN | NO | Estado activo (default: true) | +| `created_by` | UUID | SI | Usuario que creo | +| `created_at` | TIMESTAMP | NO | Fecha de creacion | +| `updated_by` | UUID | SI | Usuario que actualizo | +| `updated_at` | TIMESTAMP | SI | Fecha de actualizacion | + +### 2.4 SalesTeamMember + +**Schema:** `sales.sales_team_members` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `sales_team_id` | UUID | NO | FK a sales_teams | +| `user_id` | UUID | NO | FK a auth.users | +| `role` | VARCHAR(100) | SI | Rol en el equipo | +| `joined_at` | TIMESTAMP | NO | Fecha de union | + +### 2.5 Pricelist + +**Schema:** `sales.pricelists` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `tenant_id` | UUID | NO | Tenant | +| `company_id` | UUID | SI | FK a auth.companies | +| `name` | VARCHAR(255) | NO | Nombre de la lista | +| `currency_id` | UUID | NO | FK a core.currencies | +| `active` | BOOLEAN | NO | Estado activo (default: true) | +| `created_by` | UUID | SI | Usuario que creo | +| `created_at` | TIMESTAMP | NO | Fecha de creacion | +| `updated_by` | UUID | SI | Usuario que actualizo | +| `updated_at` | TIMESTAMP | SI | Fecha de actualizacion | + +### 2.6 PricelistItem + +**Schema:** `sales.pricelist_items` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `pricelist_id` | UUID | NO | FK a pricelists | +| `product_id` | UUID | SI | FK a inventory.products | +| `product_category_id` | UUID | SI | FK a core.product_categories | +| `price` | DECIMAL | NO | Precio | +| `min_quantity` | INTEGER | NO | Cantidad minima (default: 1) | +| `valid_from` | DATE | SI | Fecha inicio validez | +| `valid_to` | DATE | SI | Fecha fin validez | +| `active` | BOOLEAN | NO | Estado activo (default: true) | +| `created_by` | UUID | SI | Usuario que creo | + +### 2.7 SalesOrder + +**Schema:** `sales.sales_orders` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `tenant_id` | UUID | NO | Tenant | +| `company_id` | UUID | NO | FK a auth.companies | +| `name` | VARCHAR(50) | NO | Numero de orden (SO-XXXXXX) | +| `client_order_ref` | VARCHAR(100) | SI | Referencia del cliente | +| `partner_id` | UUID | NO | FK a core.partners | +| `order_date` | DATE | NO | Fecha de orden | +| `validity_date` | DATE | SI | Fecha de validez | +| `commitment_date` | DATE | SI | Fecha de compromiso | +| `currency_id` | UUID | NO | FK a core.currencies | +| `pricelist_id` | UUID | SI | FK a pricelists | +| `payment_term_id` | UUID | SI | FK a financial.payment_terms | +| `user_id` | UUID | SI | FK a auth.users (vendedor) | +| `sales_team_id` | UUID | SI | FK a sales_teams | +| `amount_untaxed` | DECIMAL | NO | Subtotal sin impuestos | +| `amount_tax` | DECIMAL | NO | Total impuestos | +| `amount_total` | DECIMAL | NO | Total | +| `status` | ENUM | NO | draft, sent, sale, done, cancelled | +| `invoice_status` | ENUM | NO | pending, partial, invoiced | +| `delivery_status` | ENUM | NO | pending, partial, delivered | +| `invoice_policy` | ENUM | NO | order, delivery | +| `picking_id` | UUID | SI | FK a inventory.pickings | +| `notes` | TEXT | SI | Notas internas | +| `terms_conditions` | TEXT | SI | Terminos y condiciones | +| `created_by` | UUID | SI | Usuario que creo | +| `created_at` | TIMESTAMP | NO | Fecha de creacion | +| `confirmed_at` | TIMESTAMP | SI | Fecha de confirmacion | +| `confirmed_by` | UUID | SI | Usuario que confirmo | +| `cancelled_at` | TIMESTAMP | SI | Fecha de cancelacion | +| `cancelled_by` | UUID | SI | Usuario que cancelo | +| `updated_by` | UUID | SI | Usuario que actualizo | +| `updated_at` | TIMESTAMP | SI | Fecha de actualizacion | + +### 2.8 SalesOrderLine + +**Schema:** `sales.sales_order_lines` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `order_id` | UUID | NO | FK a sales_orders | +| `tenant_id` | UUID | NO | Tenant | +| `product_id` | UUID | NO | FK a inventory.products | +| `description` | TEXT | NO | Descripcion | +| `quantity` | DECIMAL | NO | Cantidad | +| `qty_delivered` | DECIMAL | NO | Cantidad entregada | +| `qty_invoiced` | DECIMAL | NO | Cantidad facturada | +| `uom_id` | UUID | NO | FK a core.uom | +| `price_unit` | DECIMAL | NO | Precio unitario | +| `discount` | DECIMAL | NO | Porcentaje descuento | +| `tax_ids` | UUID[] | NO | Array de FK a financial.taxes | +| `amount_untaxed` | DECIMAL | NO | Subtotal sin impuestos | +| `amount_tax` | DECIMAL | NO | Total impuestos | +| `amount_total` | DECIMAL | NO | Total | +| `analytic_account_id` | UUID | SI | FK a financial.analytic_accounts | +| `created_at` | TIMESTAMP | NO | Fecha de creacion | +| `updated_at` | TIMESTAMP | SI | Fecha de actualizacion | + +### 2.9 Quotation + +**Schema:** `sales.quotations` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `tenant_id` | UUID | NO | Tenant | +| `company_id` | UUID | NO | FK a auth.companies | +| `name` | VARCHAR(50) | NO | Numero de cotizacion (QUO-XXXXXX) | +| `partner_id` | UUID | NO | FK a core.partners | +| `quotation_date` | DATE | NO | Fecha de cotizacion | +| `validity_date` | DATE | NO | Fecha de validez | +| `currency_id` | UUID | NO | FK a core.currencies | +| `pricelist_id` | UUID | SI | FK a pricelists | +| `user_id` | UUID | SI | FK a auth.users (vendedor) | +| `sales_team_id` | UUID | SI | FK a sales_teams | +| `amount_untaxed` | DECIMAL | NO | Subtotal sin impuestos | +| `amount_tax` | DECIMAL | NO | Total impuestos | +| `amount_total` | DECIMAL | NO | Total | +| `status` | ENUM | NO | draft, sent, confirmed, cancelled, expired | +| `sale_order_id` | UUID | SI | FK a sales_orders (orden generada) | +| `notes` | TEXT | SI | Notas internas | +| `terms_conditions` | TEXT | SI | Terminos y condiciones | +| `created_by` | UUID | SI | Usuario que creo | +| `created_at` | TIMESTAMP | NO | Fecha de creacion | +| `updated_by` | UUID | SI | Usuario que actualizo | +| `updated_at` | TIMESTAMP | SI | Fecha de actualizacion | + +### 2.10 QuotationLine + +**Schema:** `sales.quotation_lines` + +| Columna | Tipo | Nullable | Descripcion | +|---------|------|----------|-------------| +| `id` | UUID | NO | Identificador unico | +| `quotation_id` | UUID | NO | FK a quotations | +| `tenant_id` | UUID | NO | Tenant | +| `product_id` | UUID | SI | FK a inventory.products | +| `description` | TEXT | NO | Descripcion | +| `quantity` | DECIMAL | NO | Cantidad | +| `uom_id` | UUID | NO | FK a core.uom | +| `price_unit` | DECIMAL | NO | Precio unitario | +| `discount` | DECIMAL | NO | Porcentaje descuento | +| `tax_ids` | UUID[] | NO | Array de FK a financial.taxes | +| `amount_untaxed` | DECIMAL | NO | Subtotal sin impuestos | +| `amount_tax` | DECIMAL | NO | Total impuestos | +| `amount_total` | DECIMAL | NO | Total | +| `created_at` | TIMESTAMP | NO | Fecha de creacion | + +--- + +## 3. DTOs + +### 3.1 CustomerGroups DTOs + +```typescript +interface CreateCustomerGroupDto { + name: string; // Requerido, max 255 + description?: string; // Opcional + discount_percentage?: number; // 0-100, default: 0 +} + +interface UpdateCustomerGroupDto { + name?: string; // max 255 + description?: string | null; + discount_percentage?: number; // 0-100 +} + +interface CustomerGroupFilters { + search?: string; + page?: number; // default: 1 + limit?: number; // default: 20, max: 100 +} +``` + +### 3.2 SalesTeams DTOs + +```typescript +interface CreateSalesTeamDto { + company_id: string; // UUID, requerido + name: string; // Requerido, max 255 + code?: string; // max 50 + team_leader_id?: string; // UUID + target_monthly?: number; // > 0 + target_annual?: number; // > 0 +} + +interface UpdateSalesTeamDto { + name?: string; + code?: string; + team_leader_id?: string | null; + target_monthly?: number | null; + target_annual?: number | null; + active?: boolean; +} + +interface SalesTeamFilters { + company_id?: string; + active?: boolean; + page?: number; + limit?: number; +} +``` + +### 3.3 Pricelists DTOs + +```typescript +interface CreatePricelistDto { + company_id?: string; // UUID + name: string; // Requerido, max 255 + currency_id: string; // UUID, requerido +} + +interface UpdatePricelistDto { + name?: string; + currency_id?: string; + active?: boolean; +} + +interface CreatePricelistItemDto { + product_id?: string; // UUID (uno de product_id o product_category_id) + product_category_id?: string; // UUID + price: number; // >= 0 + min_quantity?: number; // > 0, default: 1 + valid_from?: string; // ISO date + valid_to?: string; // ISO date +} + +interface PricelistFilters { + company_id?: string; + active?: boolean; + page?: number; + limit?: number; +} +``` + +### 3.4 Orders DTOs + +```typescript +interface CreateSalesOrderDto { + company_id: string; // UUID, requerido + partner_id: string; // UUID, requerido + client_order_ref?: string; // max 100 + order_date?: string; // ISO date + validity_date?: string; // ISO date + commitment_date?: string; // ISO date + currency_id: string; // UUID, requerido + pricelist_id?: string; // UUID + payment_term_id?: string; // UUID + sales_team_id?: string; // UUID + invoice_policy?: 'order' | 'delivery'; // default: 'order' + notes?: string; + terms_conditions?: string; +} + +interface UpdateSalesOrderDto { + partner_id?: string; + client_order_ref?: string | null; + order_date?: string; + validity_date?: string | null; + commitment_date?: string | null; + currency_id?: string; + pricelist_id?: string | null; + payment_term_id?: string | null; + sales_team_id?: string | null; + invoice_policy?: 'order' | 'delivery'; + notes?: string | null; + terms_conditions?: string | null; +} + +interface CreateSalesOrderLineDto { + product_id: string; // UUID, requerido + description: string; // Requerido + quantity: number; // > 0 + uom_id: string; // UUID, requerido + price_unit: number; // >= 0 + discount?: number; // 0-100, default: 0 + tax_ids?: string[]; // UUID[] + analytic_account_id?: string; // UUID +} + +interface UpdateSalesOrderLineDto { + description?: string; + quantity?: number; + uom_id?: string; + price_unit?: number; + discount?: number; + tax_ids?: string[]; + analytic_account_id?: string | null; +} + +interface SalesOrderFilters { + company_id?: string; + partner_id?: string; + status?: 'draft' | 'sent' | 'sale' | 'done' | 'cancelled'; + invoice_status?: 'pending' | 'partial' | 'invoiced'; + delivery_status?: 'pending' | 'partial' | 'delivered'; + date_from?: string; + date_to?: string; + search?: string; + page?: number; + limit?: number; +} +``` + +### 3.5 Quotations DTOs + +```typescript +interface CreateQuotationDto { + company_id: string; // UUID, requerido + partner_id: string; // UUID, requerido + quotation_date?: string; // ISO date + validity_date: string; // ISO date, requerido + currency_id: string; // UUID, requerido + pricelist_id?: string; // UUID + sales_team_id?: string; // UUID + notes?: string; + terms_conditions?: string; +} + +interface UpdateQuotationDto { + partner_id?: string; + quotation_date?: string; + validity_date?: string; + currency_id?: string; + pricelist_id?: string | null; + sales_team_id?: string | null; + notes?: string | null; + terms_conditions?: string | null; +} + +interface CreateQuotationLineDto { + product_id?: string; // UUID + description: string; // Requerido + quantity: number; // > 0 + uom_id: string; // UUID, requerido + price_unit: number; // >= 0 + discount?: number; // 0-100, default: 0 + tax_ids?: string[]; // UUID[] +} + +interface UpdateQuotationLineDto { + description?: string; + quantity?: number; + uom_id?: string; + price_unit?: number; + discount?: number; + tax_ids?: string[]; +} + +interface QuotationFilters { + company_id?: string; + partner_id?: string; + status?: 'draft' | 'sent' | 'confirmed' | 'cancelled' | 'expired'; + date_from?: string; + date_to?: string; + search?: string; + page?: number; + limit?: number; +} +``` + +--- + +## 4. ENDPOINTS + +### 4.1 Customer Groups + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| GET | `/api/sales/customer-groups` | Listar grupos de clientes | +| GET | `/api/sales/customer-groups/:id` | Obtener grupo por ID | +| POST | `/api/sales/customer-groups` | Crear grupo de clientes | +| PATCH | `/api/sales/customer-groups/:id` | Actualizar grupo | +| DELETE | `/api/sales/customer-groups/:id` | Eliminar grupo | +| POST | `/api/sales/customer-groups/:id/members` | Agregar miembro | +| DELETE | `/api/sales/customer-groups/:id/members/:memberId` | Eliminar miembro | + +### 4.2 Sales Teams + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| GET | `/api/sales/teams` | Listar equipos de ventas | +| GET | `/api/sales/teams/:id` | Obtener equipo por ID | +| POST | `/api/sales/teams` | Crear equipo de ventas | +| PATCH | `/api/sales/teams/:id` | Actualizar equipo | +| POST | `/api/sales/teams/:id/members` | Agregar miembro | +| DELETE | `/api/sales/teams/:id/members/:memberId` | Eliminar miembro | + +### 4.3 Pricelists + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| GET | `/api/sales/pricelists` | Listar listas de precios | +| GET | `/api/sales/pricelists/:id` | Obtener lista por ID | +| POST | `/api/sales/pricelists` | Crear lista de precios | +| PATCH | `/api/sales/pricelists/:id` | Actualizar lista | +| POST | `/api/sales/pricelists/:id/items` | Agregar item | +| DELETE | `/api/sales/pricelists/:id/items/:itemId` | Eliminar item | + +### 4.4 Sales Orders + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| GET | `/api/sales/orders` | Listar ordenes de venta | +| GET | `/api/sales/orders/:id` | Obtener orden por ID | +| POST | `/api/sales/orders` | Crear orden de venta | +| PATCH | `/api/sales/orders/:id` | Actualizar orden | +| DELETE | `/api/sales/orders/:id` | Eliminar orden | +| POST | `/api/sales/orders/:id/lines` | Agregar linea | +| PATCH | `/api/sales/orders/:id/lines/:lineId` | Actualizar linea | +| DELETE | `/api/sales/orders/:id/lines/:lineId` | Eliminar linea | +| POST | `/api/sales/orders/:id/confirm` | Confirmar orden | +| POST | `/api/sales/orders/:id/cancel` | Cancelar orden | +| POST | `/api/sales/orders/:id/invoice` | Crear factura | + +### 4.5 Quotations + +| Metodo | Endpoint | Descripcion | +|--------|----------|-------------| +| GET | `/api/sales/quotations` | Listar cotizaciones | +| GET | `/api/sales/quotations/:id` | Obtener cotizacion por ID | +| POST | `/api/sales/quotations` | Crear cotizacion | +| PATCH | `/api/sales/quotations/:id` | Actualizar cotizacion | +| DELETE | `/api/sales/quotations/:id` | Eliminar cotizacion | +| POST | `/api/sales/quotations/:id/lines` | Agregar linea | +| PATCH | `/api/sales/quotations/:id/lines/:lineId` | Actualizar linea | +| DELETE | `/api/sales/quotations/:id/lines/:lineId` | Eliminar linea | +| POST | `/api/sales/quotations/:id/send` | Enviar cotizacion | +| POST | `/api/sales/quotations/:id/confirm` | Confirmar y crear orden | +| POST | `/api/sales/quotations/:id/cancel` | Cancelar cotizacion | + +--- + +## 5. TESTS + +### 5.1 Estado de Tests + +| Servicio | Archivo | Estado | +|----------|---------|--------| +| CustomerGroupsService | `__tests__/customer-groups.service.spec.ts` | Implementado | +| SalesTeamsService | `__tests__/sales-teams.service.spec.ts` | Implementado | +| PricelistsService | `__tests__/pricelists.service.spec.ts` | Implementado | +| OrdersService | `__tests__/orders.service.spec.ts` | Implementado | +| QuotationsService | `__tests__/quotations.service.spec.ts` | Implementado | + +### 5.2 Cobertura + +Todos los servicios cuentan con tests unitarios que cubren: +- CRUD basico +- Validaciones de negocio +- Manejo de errores +- Flujos de estado (confirmar, cancelar, etc.) + +--- + +## 6. DEPENDENCIAS + +### 6.1 Modulos Internos + +| Modulo | Uso | +|--------|-----| +| `auth` | Usuarios, empresas, autenticacion | +| `core` | Partners, currencies, UoM, product_categories, sequences | +| `inventory` | Products | +| `financial` | Taxes (calculo impuestos), invoices, payment_terms, analytic_accounts | + +### 6.2 Servicios Externos + +| Servicio | Uso | +|----------|-----| +| `taxesService` | Calculo de impuestos en lineas de orden/cotizacion | +| `sequencesService` | Generacion de numeros de secuencia (SO-XXXXXX) | +| `emailService` | Envio de cotizaciones por email | + +### 6.3 Dependencias de Base de Datos + +``` +sales.customer_groups + └── core.partners (via customer_group_members) + +sales.sales_teams + ├── auth.companies + └── auth.users (team_leader, members) + +sales.pricelists + ├── auth.companies + ├── core.currencies + ├── inventory.products (items) + └── core.product_categories (items) + +sales.sales_orders + ├── auth.companies + ├── core.partners + ├── core.currencies + ├── sales.pricelists + ├── sales.sales_teams + ├── auth.users + ├── financial.payment_terms + ├── inventory.products (lines) + ├── core.uom (lines) + └── financial.taxes (lines) + +sales.quotations + ├── (mismas dependencias que sales_orders) + └── sales.sales_orders (sale_order_id) +``` + +--- + +## 7. FLUJOS DE NEGOCIO + +### 7.1 Flujo de Cotizacion + +``` +draft -> sent -> confirmed -> [crea sales_order] + -> cancelled + -> expired +``` + +### 7.2 Flujo de Orden de Venta + +``` +draft -> sent -> sale -> done + -> cancelled + +invoice_status: pending -> partial -> invoiced +delivery_status: pending -> partial -> delivered +``` + +### 7.3 Reglas de Negocio + +1. **Ordenes/Cotizaciones**: Solo se pueden editar/eliminar en estado `draft` +2. **Grupos de clientes**: No se pueden eliminar si tienen miembros +3. **Equipos de ventas**: Codigo unico por empresa +4. **Listas de precios**: Nombre unico por tenant +5. **Facturacion**: Segun `invoice_policy` (order: al confirmar, delivery: al entregar) +6. **Impuestos**: Calculados automaticamente usando `taxesService` + +--- + +## 8. VALIDACIONES + +### 8.1 Validaciones con Zod + +Todas las validaciones de entrada se realizan usando Zod en el controlador: +- Tipos de datos +- Campos requeridos +- Rangos (min, max) +- Formatos (UUID, email, fechas) +- Valores permitidos (enums) + +### 8.2 Validaciones de Negocio + +Las validaciones de negocio se realizan en los servicios: +- Unicidad de nombres/codigos +- Estados validos para operaciones +- Existencia de entidades relacionadas +- Restricciones de eliminacion diff --git a/docs/03-fase-vertical/MGN-012-purchases/README.md b/docs/03-fase-vertical/MGN-012-purchases/README.md new file mode 100644 index 0000000..c6f9085 --- /dev/null +++ b/docs/03-fase-vertical/MGN-012-purchases/README.md @@ -0,0 +1,44 @@ +# MGN-012: Purchases (Gestión de Compras) + +## Descripción + +Módulo de gestión del ciclo de compras, incluyendo solicitudes de cotización (RFQ) y órdenes de compra. + +## Implementación Backend + +**Ubicación:** `/backend/src/modules/purchases/` + +### Servicios Implementados + +| Servicio | Descripción | +|----------|-------------| +| `purchases.service.ts` | Órdenes de compra | +| `rfqs.service.ts` | Solicitudes de cotización (RFQ) | + +### Estados de Documentos + +- `draft` - Borrador +- `sent` - Enviado al proveedor +- `confirmed` - Confirmado +- `done` - Completado +- `cancelled` - Cancelado + +## Dependencias + +- **MGN-017 (Partners):** Proveedores +- **MGN-013 (Inventory):** Productos +- **MGN-010 (Financial):** Facturación, impuestos +- **MGN-005 (Catalogs):** Monedas, UOM + +## Estado de Documentación + +| Artefacto | Estado | +|-----------|--------| +| README | Básico | +| Requerimientos Funcionales | Pendiente | +| Especificaciones Técnicas | Pendiente | +| User Stories | Pendiente | + +--- + +*Módulo identificado durante sincronización docs-código: 2026-01-10* diff --git a/docs/03-fase-vertical/MGN-012-purchases/especificaciones/ET-PURCHASES-BACKEND.md b/docs/03-fase-vertical/MGN-012-purchases/especificaciones/ET-PURCHASES-BACKEND.md new file mode 100644 index 0000000..bcd513a --- /dev/null +++ b/docs/03-fase-vertical/MGN-012-purchases/especificaciones/ET-PURCHASES-BACKEND.md @@ -0,0 +1,597 @@ +# Especificacion Tecnica Backend - Modulo Purchases (MGN-012) + +## METADATOS + +| Campo | Valor | +|-------|-------| +| **Modulo** | MGN-012 | +| **Nombre** | Purchases (Compras) | +| **Version** | 1.0.0 | +| **Fecha** | 2026-01-10 | +| **Estado** | Implementado | +| **Backend Path** | `backend/src/modules/purchases/` | +| **Schema BD** | `purchase` | + +--- + +## SERVICIOS + +### 1. PurchasesService + +**Archivo:** `purchases.service.ts` + +**Descripcion:** Servicio para gestion de ordenes de compra (Purchase Orders). + +#### Metodos + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string`, `filters: PurchaseOrderFilters` | `Promise<{ data: PurchaseOrder[]; total: number }>` | Lista ordenes de compra con paginacion y filtros | +| `findById` | `id: string`, `tenantId: string` | `Promise` | Obtiene una orden por ID con sus lineas | +| `create` | `dto: CreatePurchaseOrderDto`, `tenantId: string`, `userId: string` | `Promise` | Crea nueva orden de compra con lineas (transaccion) | +| `update` | `id: string`, `dto: UpdatePurchaseOrderDto`, `tenantId: string`, `userId: string` | `Promise` | Actualiza orden en estado draft | +| `confirm` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Confirma orden (draft -> confirmed) | +| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Cancela orden (no aplica a done) | +| `delete` | `id: string`, `tenantId: string` | `Promise` | Elimina orden en estado draft | + +#### Tipos de Estado (OrderStatus) + +```typescript +type OrderStatus = 'draft' | 'sent' | 'confirmed' | 'done' | 'cancelled'; +``` + +#### Filtros Disponibles (PurchaseOrderFilters) + +```typescript +interface PurchaseOrderFilters { + company_id?: string; + partner_id?: string; + status?: OrderStatus; + date_from?: string; + date_to?: string; + search?: string; + page?: number; // default: 1 + limit?: number; // default: 20 +} +``` + +--- + +### 2. RfqsService + +**Archivo:** `rfqs.service.ts` + +**Descripcion:** Servicio para gestion de Solicitudes de Cotizacion (Request for Quotation - RFQ). + +#### Metodos + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string`, `filters: RfqFilters` | `Promise<{ data: Rfq[]; total: number }>` | Lista RFQs con paginacion y filtros | +| `findById` | `id: string`, `tenantId: string` | `Promise` | Obtiene RFQ por ID con lineas y partners | +| `create` | `dto: CreateRfqDto`, `tenantId: string`, `userId: string` | `Promise` | Crea nueva RFQ con lineas (transaccion) | +| `update` | `id: string`, `dto: UpdateRfqDto`, `tenantId: string`, `userId: string` | `Promise` | Actualiza RFQ en estado draft | +| `addLine` | `rfqId: string`, `dto: CreateRfqLineDto`, `tenantId: string` | `Promise` | Agrega linea a RFQ en draft | +| `updateLine` | `rfqId: string`, `lineId: string`, `dto: UpdateRfqLineDto`, `tenantId: string` | `Promise` | Actualiza linea de RFQ en draft | +| `removeLine` | `rfqId: string`, `lineId: string`, `tenantId: string` | `Promise` | Elimina linea (minimo 1 linea requerida) | +| `send` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Envia RFQ (draft -> sent) | +| `markResponded` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Marca como respondida (sent -> responded) | +| `accept` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Acepta RFQ (sent/responded -> accepted) | +| `reject` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Rechaza RFQ (sent/responded -> rejected) | +| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise` | Cancela RFQ (no aplica a accepted) | +| `delete` | `id: string`, `tenantId: string` | `Promise` | Elimina RFQ en estado draft | + +#### Tipos de Estado (RfqStatus) + +```typescript +type RfqStatus = 'draft' | 'sent' | 'responded' | 'accepted' | 'rejected' | 'cancelled'; +``` + +#### Filtros Disponibles (RfqFilters) + +```typescript +interface RfqFilters { + company_id?: string; + status?: RfqStatus; + date_from?: string; + date_to?: string; + search?: string; + page?: number; // default: 1 + limit?: number; // default: 20 +} +``` + +--- + +## ENTIDADES + +### Schema: `purchase` + +#### Tabla: purchase_orders + +| Columna | Tipo | Nullable | Default | Descripcion | +|---------|------|----------|---------|-------------| +| `id` | UUID | NO | gen_random_uuid() | Clave primaria | +| `tenant_id` | UUID | NO | - | FK a auth.tenants | +| `company_id` | UUID | NO | - | FK a auth.companies | +| `name` | VARCHAR(100) | NO | - | Numero de orden (unico por company) | +| `ref` | VARCHAR(100) | SI | - | Referencia del proveedor | +| `partner_id` | UUID | NO | - | FK a core.partners (proveedor) | +| `order_date` | DATE | NO | - | Fecha de la orden | +| `expected_date` | DATE | SI | - | Fecha esperada de recepcion | +| `effective_date` | DATE | SI | - | Fecha efectiva de recepcion | +| `currency_id` | UUID | NO | - | FK a core.currencies | +| `payment_term_id` | UUID | SI | - | FK a financial.payment_terms | +| `amount_untaxed` | DECIMAL(15,2) | NO | 0 | Monto sin impuestos | +| `amount_tax` | DECIMAL(15,2) | NO | 0 | Monto de impuestos | +| `amount_total` | DECIMAL(15,2) | NO | 0 | Monto total | +| `status` | order_status | NO | 'draft' | Estado de la orden | +| `receipt_status` | VARCHAR(20) | SI | 'pending' | Estado de recepcion | +| `invoice_status` | VARCHAR(20) | SI | 'pending' | Estado de facturacion | +| `picking_id` | UUID | SI | - | FK a inventory.pickings | +| `invoice_id` | UUID | SI | - | FK a financial.invoices | +| `notes` | TEXT | SI | - | Notas adicionales | +| `dest_address_id` | UUID | SI | - | Direccion de envio (dropship) | +| `locked` | BOOLEAN | SI | FALSE | Bloqueo de orden | +| `approval_required` | BOOLEAN | SI | FALSE | Requiere aprobacion | +| `amount_approval_threshold` | DECIMAL(15,2) | SI | - | Umbral de aprobacion | +| `created_at` | TIMESTAMP | NO | CURRENT_TIMESTAMP | Fecha creacion | +| `created_by` | UUID | SI | - | Usuario creador | +| `updated_at` | TIMESTAMP | SI | - | Fecha actualizacion | +| `updated_by` | UUID | SI | - | Usuario actualizacion | +| `confirmed_at` | TIMESTAMP | SI | - | Fecha confirmacion | +| `confirmed_by` | UUID | SI | - | Usuario confirmacion | +| `approved_at` | TIMESTAMP | SI | - | Fecha aprobacion | +| `approved_by` | UUID | SI | - | Usuario aprobacion | +| `cancelled_at` | TIMESTAMP | SI | - | Fecha cancelacion | +| `cancelled_by` | UUID | SI | - | Usuario cancelacion | + +**Indices:** +- `idx_purchase_orders_tenant_id` +- `idx_purchase_orders_company_id` +- `idx_purchase_orders_partner_id` +- `idx_purchase_orders_name` +- `idx_purchase_orders_status` +- `idx_purchase_orders_order_date` +- `idx_purchase_orders_expected_date` + +**Constraint UNIQUE:** `(company_id, name)` + +--- + +#### Tabla: purchase_order_lines + +| Columna | Tipo | Nullable | Default | Descripcion | +|---------|------|----------|---------|-------------| +| `id` | UUID | NO | gen_random_uuid() | Clave primaria | +| `tenant_id` | UUID | NO | - | FK a auth.tenants | +| `order_id` | UUID | NO | - | FK a purchase.purchase_orders | +| `product_id` | UUID | NO | - | FK a inventory.products | +| `description` | TEXT | NO | - | Descripcion del producto | +| `quantity` | DECIMAL(12,4) | NO | - | Cantidad solicitada | +| `qty_received` | DECIMAL(12,4) | SI | 0 | Cantidad recibida | +| `qty_invoiced` | DECIMAL(12,4) | SI | 0 | Cantidad facturada | +| `uom_id` | UUID | NO | - | FK a core.uom | +| `price_unit` | DECIMAL(15,4) | NO | - | Precio unitario | +| `discount` | DECIMAL(5,2) | SI | 0 | Porcentaje descuento | +| `tax_ids` | UUID[] | SI | '{}' | Array de impuestos | +| `amount_untaxed` | DECIMAL(15,2) | NO | - | Subtotal sin impuestos | +| `amount_tax` | DECIMAL(15,2) | NO | - | Monto impuestos | +| `amount_total` | DECIMAL(15,2) | NO | - | Total linea | +| `expected_date` | DATE | SI | - | Fecha esperada | +| `analytic_account_id` | UUID | SI | - | FK a analytics.analytic_accounts | +| `created_at` | TIMESTAMP | NO | CURRENT_TIMESTAMP | Fecha creacion | +| `updated_at` | TIMESTAMP | SI | - | Fecha actualizacion | + +**Constraints:** +- `chk_purchase_order_lines_quantity`: quantity > 0 +- `chk_purchase_order_lines_discount`: discount >= 0 AND discount <= 100 + +--- + +#### Tabla: rfqs + +| Columna | Tipo | Nullable | Default | Descripcion | +|---------|------|----------|---------|-------------| +| `id` | UUID | NO | gen_random_uuid() | Clave primaria | +| `tenant_id` | UUID | NO | - | FK a auth.tenants | +| `company_id` | UUID | NO | - | FK a auth.companies | +| `name` | VARCHAR(100) | NO | - | Numero RFQ (ej: RFQ-000001) | +| `partner_ids` | UUID[] | NO | - | Array de proveedores | +| `request_date` | DATE | NO | - | Fecha de solicitud | +| `deadline_date` | DATE | SI | - | Fecha limite respuesta | +| `response_date` | DATE | SI | - | Fecha de respuesta | +| `status` | rfq_status | NO | 'draft' | Estado del RFQ | +| `description` | TEXT | SI | - | Descripcion | +| `notes` | TEXT | SI | - | Notas adicionales | +| `created_at` | TIMESTAMP | NO | CURRENT_TIMESTAMP | Fecha creacion | +| `created_by` | UUID | SI | - | Usuario creador | +| `updated_at` | TIMESTAMP | SI | - | Fecha actualizacion | +| `updated_by` | UUID | SI | - | Usuario actualizacion | + +**Constraint UNIQUE:** `(company_id, name)` + +--- + +#### Tabla: rfq_lines + +| Columna | Tipo | Nullable | Default | Descripcion | +|---------|------|----------|---------|-------------| +| `id` | UUID | NO | gen_random_uuid() | Clave primaria | +| `tenant_id` | UUID | NO | - | FK a auth.tenants | +| `rfq_id` | UUID | NO | - | FK a purchase.rfqs | +| `product_id` | UUID | SI | - | FK a inventory.products | +| `description` | TEXT | NO | - | Descripcion del producto | +| `quantity` | DECIMAL(12,4) | NO | - | Cantidad solicitada | +| `uom_id` | UUID | NO | - | FK a core.uom | +| `created_at` | TIMESTAMP | NO | CURRENT_TIMESTAMP | Fecha creacion | + +**Constraint:** `chk_rfq_lines_quantity`: quantity > 0 + +--- + +## DTOs + +### Purchase Orders + +#### CreatePurchaseOrderDto + +```typescript +interface CreatePurchaseOrderDto { + company_id: string; // UUID, requerido + name: string; // min: 1, max: 100 + ref?: string; // max: 100 + partner_id: string; // UUID, requerido + order_date: string; // formato: YYYY-MM-DD + expected_date?: string; // formato: YYYY-MM-DD + currency_id: string; // UUID, requerido + payment_term_id?: string; // UUID + notes?: string; + lines: PurchaseOrderLineDto[]; // min: 1 linea +} + +interface PurchaseOrderLineDto { + product_id: string; // UUID, requerido + description: string; // min: 1 + quantity: number; // positive + uom_id: string; // UUID, requerido + price_unit: number; // min: 0 + discount?: number; // 0-100, default: 0 + amount_untaxed: number; // min: 0 +} +``` + +#### UpdatePurchaseOrderDto + +```typescript +interface UpdatePurchaseOrderDto { + ref?: string | null; + partner_id?: string; + order_date?: string; + expected_date?: string | null; + currency_id?: string; + payment_term_id?: string | null; + notes?: string | null; + lines?: PurchaseOrderLineDto[]; // min: 1 si se proporciona +} +``` + +### RFQs + +#### CreateRfqDto + +```typescript +interface CreateRfqDto { + company_id: string; // UUID, requerido + partner_ids: string[]; // Array UUID, min: 1 + request_date?: string; // YYYY-MM-DD, default: hoy + deadline_date?: string; // YYYY-MM-DD + description?: string; + notes?: string; + lines: CreateRfqLineDto[]; // min: 1 linea +} + +interface CreateRfqLineDto { + product_id?: string; // UUID, opcional + description: string; // min: 1 + quantity: number; // positive + uom_id: string; // UUID, requerido +} +``` + +#### UpdateRfqDto + +```typescript +interface UpdateRfqDto { + partner_ids?: string[]; // Array UUID, min: 1 si se proporciona + deadline_date?: string | null; + description?: string | null; + notes?: string | null; +} +``` + +#### UpdateRfqLineDto + +```typescript +interface UpdateRfqLineDto { + product_id?: string | null; + description?: string; + quantity?: number; + uom_id?: string; +} +``` + +--- + +## ENDPOINTS + +### Base Path: `/api/purchases` + +#### Purchase Orders + +| Metodo | Ruta | Roles Permitidos | Descripcion | +|--------|------|------------------|-------------| +| GET | `/` | admin, manager, warehouse, accountant, super_admin | Listar ordenes de compra | +| GET | `/:id` | admin, manager, warehouse, accountant, super_admin | Obtener orden por ID | +| POST | `/` | admin, manager, warehouse, super_admin | Crear orden de compra | +| PUT | `/:id` | admin, manager, warehouse, super_admin | Actualizar orden | +| POST | `/:id/confirm` | admin, manager, super_admin | Confirmar orden | +| POST | `/:id/cancel` | admin, manager, super_admin | Cancelar orden | +| DELETE | `/:id` | admin, super_admin | Eliminar orden (solo draft) | + +#### RFQs (Request for Quotation) + +| Metodo | Ruta | Roles Permitidos | Descripcion | +|--------|------|------------------|-------------| +| GET | `/rfqs` | admin, manager, warehouse, super_admin | Listar RFQs | +| GET | `/rfqs/:id` | admin, manager, warehouse, super_admin | Obtener RFQ por ID | +| POST | `/rfqs` | admin, manager, warehouse, super_admin | Crear RFQ | +| PUT | `/rfqs/:id` | admin, manager, warehouse, super_admin | Actualizar RFQ | +| DELETE | `/rfqs/:id` | admin, super_admin | Eliminar RFQ (solo draft) | + +#### RFQ Lines + +| Metodo | Ruta | Roles Permitidos | Descripcion | +|--------|------|------------------|-------------| +| POST | `/rfqs/:id/lines` | admin, manager, warehouse, super_admin | Agregar linea a RFQ | +| PUT | `/rfqs/:id/lines/:lineId` | admin, manager, warehouse, super_admin | Actualizar linea | +| DELETE | `/rfqs/:id/lines/:lineId` | admin, manager, warehouse, super_admin | Eliminar linea | + +#### RFQ Workflow + +| Metodo | Ruta | Roles Permitidos | Descripcion | +|--------|------|------------------|-------------| +| POST | `/rfqs/:id/send` | admin, manager, super_admin | Enviar RFQ a proveedores | +| POST | `/rfqs/:id/responded` | admin, manager, super_admin | Marcar como respondida | +| POST | `/rfqs/:id/accept` | admin, manager, super_admin | Aceptar RFQ | +| POST | `/rfqs/:id/reject` | admin, manager, super_admin | Rechazar RFQ | +| POST | `/rfqs/:id/cancel` | admin, manager, super_admin | Cancelar RFQ | + +### Formato de Respuesta + +#### Exito (Listado) + +```json +{ + "success": true, + "data": [...], + "meta": { + "total": 100, + "page": 1, + "limit": 20, + "totalPages": 5 + } +} +``` + +#### Exito (Entidad) + +```json +{ + "success": true, + "data": { ... }, + "message": "Operacion exitosa" +} +``` + +#### Error + +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "message": "Descripcion del error", + "details": [...] + } +} +``` + +--- + +## TESTS + +### Archivo: `__tests__/purchases.service.spec.ts` + +#### Casos de Prueba - PurchasesService + +| Suite | Test Case | Estado | +|-------|-----------|--------| +| findAll | should return paginated orders for a tenant | PASS | +| findAll | should enforce tenant isolation | PASS | +| findAll | should apply pagination correctly | PASS | +| findById | should return order with lines by id | PASS | +| findById | should throw NotFoundError when order does not exist | PASS | +| findById | should enforce tenant isolation | PASS | +| create | should create order with lines in transaction | PASS | +| create | should rollback transaction on error | PASS | +| update | should update draft order successfully | PASS | +| update | should throw ValidationError when updating confirmed order | PASS | +| delete | should delete draft order successfully | PASS | +| delete | should throw ValidationError when deleting confirmed order | PASS | +| confirm | should confirm draft order successfully | PASS | +| confirm | should throw ValidationError when confirming order without lines | PASS | +| confirm | should throw ValidationError when confirming non-draft order | PASS | +| cancel | should cancel draft order successfully | PASS | +| cancel | should throw ValidationError when cancelling done order | PASS | +| cancel | should throw ValidationError when cancelling already cancelled order | PASS | + +### Archivo: `__tests__/rfqs.service.spec.ts` + +#### Casos de Prueba - RfqsService + +| Suite | Test Case | Estado | +|-------|-----------|--------| +| findAll | should return paginated RFQs for a tenant | PASS | +| findAll | should enforce tenant isolation | PASS | +| findAll | should apply pagination correctly | PASS | +| findAll | should filter by company_id, status, date range, search | PASS | +| findById | should return RFQ with lines by id | PASS | +| findById | should return partner names when partner_ids exist | PASS | +| findById | should throw NotFoundError when RFQ does not exist | PASS | +| create | should create RFQ with lines in transaction | PASS | +| create | should generate sequential RFQ name | PASS | +| create | should rollback transaction on error | PASS | +| create | should throw ValidationError when lines or partner_ids empty | PASS | +| update | should update draft RFQ successfully | PASS | +| update | should throw ValidationError when updating non-draft RFQ | PASS | +| delete | should delete draft RFQ successfully | PASS | +| delete | should throw ValidationError when deleting non-draft RFQ | PASS | +| addLine | should add line to draft RFQ successfully | PASS | +| updateLine | should update line in draft RFQ successfully | PASS | +| removeLine | should remove line from draft RFQ (min 1 required) | PASS | +| send | should send draft RFQ successfully | PASS | +| markResponded | should mark sent RFQ as responded | PASS | +| accept | should accept responded/sent RFQ | PASS | +| reject | should reject responded/sent RFQ | PASS | +| cancel | should cancel draft/sent/responded/rejected RFQ | PASS | +| Status Transitions | Validates all valid/invalid state transitions | PASS | +| Tenant Isolation | should not access RFQs from different tenant | PASS | +| Error Handling | should propagate database errors | PASS | + +--- + +## DEPENDENCIAS + +### Internas (Modulos del Sistema) + +| Modulo | Uso | +|--------|-----| +| `config/database` | Conexion a BD (query, queryOne, getClient) | +| `shared/errors` | NotFoundError, ConflictError, ValidationError | +| `shared/middleware/auth.middleware` | authenticate, requireRoles, AuthenticatedRequest | + +### Externas (npm) + +| Paquete | Uso | +|---------|-----| +| `express` | Router, Request, Response, NextFunction | +| `zod` | Validacion de DTOs y schemas | +| `pg` | Cliente PostgreSQL (via config/database) | + +### Tablas Relacionadas + +| Schema | Tabla | Relacion | +|--------|-------|----------| +| auth | tenants | tenant_id (multi-tenant) | +| auth | companies | company_id | +| auth | users | created_by, updated_by, confirmed_by | +| core | partners | partner_id (proveedores) | +| core | currencies | currency_id | +| core | uom | uom_id (unidades de medida) | +| inventory | products | product_id | +| inventory | pickings | picking_id (recepciones) | +| financial | payment_terms | payment_term_id | +| financial | invoices | invoice_id | +| analytics | analytic_accounts | analytic_account_id | + +--- + +## DIAGRAMAS + +### Flujo de Estados - Purchase Order + +``` + +--------+ + | draft | + +---+----+ + | + +--------------+---------------+ + | | | + v v v + +-------+ +---------+ +-----------+ + | sent | --> | confirmed| | cancelled | + +-------+ +----+----+ +-----------+ + | + v + +--------+ + | done | + +--------+ +``` + +### Flujo de Estados - RFQ + +``` + +--------+ + | draft | + +---+----+ + | + v + +-------+ + | sent | + +---+---+ + | + +--------------+---------------+ + | | | + v v v + +-----------+ +----------+ +-----------+ + | responded | | accepted | | rejected | + +-----+-----+ +----+-----+ +-----+-----+ + | | | + v | | + +-----------+ | | + | accepted |<------+ | + +-----------+ | + | | + v v + +-----------+ +-----------+ + | (end) | | cancelled | + +-----------+ +-----------+ +``` + +--- + +## NOTAS ADICIONALES + +### Seguridad + +1. **Multi-tenant:** Todas las consultas incluyen filtro por `tenant_id` +2. **RLS (Row Level Security):** Habilitado en todas las tablas del schema `purchase` +3. **Autenticacion:** Todos los endpoints requieren autenticacion JWT +4. **Autorizacion:** Roles especificos por endpoint + +### Validaciones de Negocio + +1. **Ordenes de Compra:** + - Solo se pueden modificar/eliminar ordenes en estado `draft` + - Minimo 1 linea requerida para confirmar + - No se puede cancelar una orden `done` + +2. **RFQs:** + - Solo se pueden modificar/eliminar RFQs en estado `draft` + - Minimo 1 proveedor (partner_id) requerido + - Minimo 1 linea requerida + - No se puede cancelar un RFQ `accepted` + +### Transacciones + +- Creacion de ordenes/RFQs con lineas utiliza transacciones (BEGIN/COMMIT/ROLLBACK) +- Rollback automatico en caso de error + +--- + +## CHANGELOG + +| Version | Fecha | Cambios | +|---------|-------|---------| +| 1.0.0 | 2026-01-10 | Version inicial - PurchasesService y RfqsService implementados | diff --git a/docs/03-fase-vertical/MGN-013-inventory/README.md b/docs/03-fase-vertical/MGN-013-inventory/README.md new file mode 100644 index 0000000..836b3c2 --- /dev/null +++ b/docs/03-fase-vertical/MGN-013-inventory/README.md @@ -0,0 +1,67 @@ +# MGN-013: Inventory (Gestión de Inventario) + +## Descripción + +Módulo completo de gestión de inventario, incluyendo almacenes, ubicaciones, movimientos de stock, lotes y valuación. + +## Implementación Backend + +**Ubicación:** `/backend/src/modules/inventory/` + +### Servicios Implementados (9) + +| Servicio | Descripción | +|----------|-------------| +| `products.service.ts` | Productos | +| `warehouses.service.ts` | Almacenes | +| `locations.service.ts` | Ubicaciones en almacén | +| `stock-quants.service.ts` | Cantidades en stock | +| `pickings.service.ts` | Selecciones/picking | +| `adjustments.service.ts` | Ajustes de inventario | +| `lots.service.ts` | Lotes de productos | +| `package-types.service.ts` | Tipos de empaques | +| `valuation.service.ts` | Valuación de inventario | + +### Entidades (11) + +- `product.entity.ts` +- `warehouse.entity.ts` +- `location.entity.ts` +- `stock-quant.entity.ts` +- `stock-move.entity.ts` +- `picking.entity.ts` +- `lot.entity.ts` +- `inventory-adjustment.entity.ts` +- `inventory-adjustment-line.entity.ts` +- `stock-valuation-layer.entity.ts` +- `package-type.entity.ts` + +### Funcionalidades + +- Gestión de almacenes multi-ubicación +- Control de stock en tiempo real +- Movimientos de inventario +- Picking y empaques +- Ajustes y recuentos +- Lotes y números seriales +- Valuación (FIFO/Promedio) +- Trazabilidad completa + +## Dependencias + +- **MGN-005 (Catalogs):** Categorías de productos, UOM + +## Estado de Documentación + +| Artefacto | Estado | +|-----------|--------| +| README | Básico | +| Requerimientos Funcionales | Pendiente | +| Especificaciones Técnicas | Pendiente | +| User Stories | Pendiente | + +**Complejidad:** MUY ALTA (9 servicios, 11 entidades) + +--- + +*Módulo identificado durante sincronización docs-código: 2026-01-10* diff --git a/docs/03-fase-vertical/MGN-014-hr/README.md b/docs/03-fase-vertical/MGN-014-hr/README.md new file mode 100644 index 0000000..21142d9 --- /dev/null +++ b/docs/03-fase-vertical/MGN-014-hr/README.md @@ -0,0 +1,50 @@ +# MGN-014: HR (Gestión de Recursos Humanos) + +## Descripción + +Módulo completo de gestión de recursos humanos, incluyendo empleados, contratos, nómina, licencias y gastos. + +## Implementación Backend + +**Ubicación:** `/backend/src/modules/hr/` + +### Servicios Implementados (7) + +| Servicio | Descripción | +|----------|-------------| +| `employees.service.ts` | Gestión de empleados | +| `departments.service.ts` | Departamentos y puestos | +| `contracts.service.ts` | Contratos laborales | +| `leaves.service.ts` | Licencias y ausencias | +| `payslips.service.ts` | Nóminas y pagos | +| `skills.service.ts` | Habilidades de empleados | +| `expenses.service.ts` | Gastos de empleados | + +### Funcionalidades + +- Registro completo de empleados +- Estructura organizacional +- Gestión de contratos +- Sistema de licencias por tipo +- Cálculo de nóminas +- Seguimiento de gastos +- Clasificación de habilidades + +## Dependencias + +- **MGN-018 (Companies):** Empresas + +## Estado de Documentación + +| Artefacto | Estado | +|-----------|--------| +| README | Básico | +| Requerimientos Funcionales | Pendiente | +| Especificaciones Técnicas | Pendiente | +| User Stories | Pendiente | + +**Complejidad:** MUY ALTA (7 servicios) + +--- + +*Módulo identificado durante sincronización docs-código: 2026-01-10* diff --git a/docs/03-fase-vertical/MGN-014-hr/especificaciones/ET-HR-BACKEND.md b/docs/03-fase-vertical/MGN-014-hr/especificaciones/ET-HR-BACKEND.md new file mode 100644 index 0000000..1eb11c2 --- /dev/null +++ b/docs/03-fase-vertical/MGN-014-hr/especificaciones/ET-HR-BACKEND.md @@ -0,0 +1,1391 @@ +# ET-HR-BACKEND - Especificacion Tecnica Backend HR + +## METADATOS + +| Campo | Valor | +|-------|-------| +| **Modulo** | MGN-014 | +| **Nombre** | Human Resources (HR) | +| **Version** | 1.0.0 | +| **Fecha** | 2026-01-10 | +| **Ubicacion** | `backend/src/modules/hr/` | +| **Schema BD** | `hr` | +| **Estado** | Implementado | + +--- + +## SERVICIOS + +### Resumen de Servicios (7) + +| # | Servicio | Archivo | Descripcion | +|---|----------|---------|-------------| +| 1 | EmployeesService | `employees.service.ts` | Gestion de empleados | +| 2 | DepartmentsService | `departments.service.ts` | Departamentos y puestos | +| 3 | ContractsService | `contracts.service.ts` | Contratos laborales | +| 4 | LeavesService | `leaves.service.ts` | Ausencias y vacaciones | +| 5 | SkillsService | `skills.service.ts` | Competencias y habilidades | +| 6 | ExpensesService | `expenses.service.ts` | Gastos de empleados | +| 7 | PayslipsService | `payslips.service.ts` | Nominas y recibos | + +--- + +### 1. EmployeesService + +**Archivo:** `employees.service.ts` + +#### Metodos + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string, filters: EmployeeFilters` | `Promise<{ data: Employee[]; total: number }>` | Lista empleados con paginacion y filtros | +| `findById` | `id: string, tenantId: string` | `Promise` | Obtiene empleado por ID | +| `create` | `dto: CreateEmployeeDto, tenantId: string, userId: string` | `Promise` | Crea nuevo empleado | +| `update` | `id: string, dto: UpdateEmployeeDto, tenantId: string, userId: string` | `Promise` | Actualiza empleado | +| `terminate` | `id: string, terminationDate: string, tenantId: string, userId: string` | `Promise` | Da de baja empleado | +| `reactivate` | `id: string, tenantId: string, userId: string` | `Promise` | Reactiva empleado | +| `delete` | `id: string, tenantId: string` | `Promise` | Elimina empleado | +| `getSubordinates` | `id: string, tenantId: string` | `Promise` | Obtiene subordinados | + +#### Reglas de Negocio + +- El `employee_number` debe ser unico por tenant +- No se puede eliminar un empleado con contratos asociados +- No se puede eliminar un empleado que es manager de otros +- Al terminar un empleado, se terminan sus contratos activos + +--- + +### 2. DepartmentsService + +**Archivo:** `departments.service.ts` + +#### Metodos - Departamentos + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string, filters: DepartmentFilters` | `Promise<{ data: Department[]; total: number }>` | Lista departamentos | +| `findById` | `id: string, tenantId: string` | `Promise` | Obtiene departamento por ID | +| `create` | `dto: CreateDepartmentDto, tenantId: string, userId: string` | `Promise` | Crea departamento | +| `update` | `id: string, dto: UpdateDepartmentDto, tenantId: string` | `Promise` | Actualiza departamento | +| `delete` | `id: string, tenantId: string` | `Promise` | Elimina departamento | + +#### Metodos - Puestos de Trabajo + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `getJobPositions` | `tenantId: string, includeInactive?: boolean` | `Promise` | Lista puestos de trabajo | +| `getJobPositionById` | `id: string, tenantId: string` | `Promise` | Obtiene puesto por ID | +| `createJobPosition` | `dto: CreateJobPositionDto, tenantId: string` | `Promise` | Crea puesto | +| `updateJobPosition` | `id: string, dto: UpdateJobPositionDto, tenantId: string` | `Promise` | Actualiza puesto | +| `deleteJobPosition` | `id: string, tenantId: string` | `Promise` | Elimina puesto | + +#### Reglas de Negocio + +- El nombre del departamento debe ser unico por empresa +- No se puede eliminar un departamento con empleados +- No se puede eliminar un departamento con subdepartamentos +- El nombre del puesto debe ser unico por tenant + +--- + +### 3. ContractsService + +**Archivo:** `contracts.service.ts` + +#### Metodos + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string, filters: ContractFilters` | `Promise<{ data: Contract[]; total: number }>` | Lista contratos | +| `findById` | `id: string, tenantId: string` | `Promise` | Obtiene contrato por ID | +| `create` | `dto: CreateContractDto, tenantId: string, userId: string` | `Promise` | Crea contrato | +| `update` | `id: string, dto: UpdateContractDto, tenantId: string, userId: string` | `Promise` | Actualiza contrato | +| `activate` | `id: string, tenantId: string, userId: string` | `Promise` | Activa contrato | +| `terminate` | `id: string, terminationDate: string, tenantId: string, userId: string` | `Promise` | Termina contrato | +| `cancel` | `id: string, tenantId: string, userId: string` | `Promise` | Cancela contrato | +| `delete` | `id: string, tenantId: string` | `Promise` | Elimina contrato | + +#### Flujo de Estados + +``` +draft -> active -> expired/terminated +draft -> cancelled +``` + +#### Reglas de Negocio + +- Un empleado solo puede tener un contrato activo +- Solo se pueden editar contratos en estado `draft` +- Solo se pueden eliminar contratos en `draft` o `cancelled` +- Al activar, actualiza el departamento y puesto del empleado + +--- + +### 4. LeavesService + +**Archivo:** `leaves.service.ts` + +#### Metodos - Tipos de Ausencia + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `getLeaveTypes` | `tenantId: string, includeInactive?: boolean` | `Promise` | Lista tipos de ausencia | +| `getLeaveTypeById` | `id: string, tenantId: string` | `Promise` | Obtiene tipo por ID | +| `createLeaveType` | `dto: CreateLeaveTypeDto, tenantId: string` | `Promise` | Crea tipo de ausencia | +| `updateLeaveType` | `id: string, dto: UpdateLeaveTypeDto, tenantId: string` | `Promise` | Actualiza tipo | +| `deleteLeaveType` | `id: string, tenantId: string` | `Promise` | Elimina tipo | + +#### Metodos - Ausencias + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAll` | `tenantId: string, filters: LeaveFilters` | `Promise<{ data: Leave[]; total: number }>` | Lista ausencias | +| `findById` | `id: string, tenantId: string` | `Promise` | Obtiene ausencia por ID | +| `create` | `dto: CreateLeaveDto, tenantId: string, userId: string` | `Promise` | Crea ausencia | +| `update` | `id: string, dto: UpdateLeaveDto, tenantId: string, userId: string` | `Promise` | Actualiza ausencia | +| `submit` | `id: string, tenantId: string, userId: string` | `Promise` | Envia solicitud | +| `approve` | `id: string, tenantId: string, userId: string` | `Promise` | Aprueba solicitud | +| `reject` | `id: string, reason: string, tenantId: string, userId: string` | `Promise` | Rechaza solicitud | +| `cancel` | `id: string, tenantId: string, userId: string` | `Promise` | Cancela solicitud | +| `delete` | `id: string, tenantId: string` | `Promise` | Elimina ausencia | + +#### Flujo de Estados + +``` +draft -> submitted -> approved + -> rejected +draft/submitted/approved -> cancelled +``` + +#### Reglas de Negocio + +- Se calcula automaticamente el numero de dias +- No se permiten ausencias solapadas +- Se respeta el maximo de dias por tipo +- Al aprobar, se actualiza el estado del empleado si corresponde + +--- + +### 5. SkillsService + +**Archivo:** `skills.service.ts` + +#### Metodos - Tipos de Habilidad + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAllSkillTypes` | `tenantId: string, filters: SkillTypeFilters` | `Promise<{ data: SkillType[]; total: number; page: number; limit: number }>` | Lista tipos | +| `findSkillTypeById` | `id: string, tenantId: string` | `Promise` | Obtiene tipo por ID | +| `createSkillType` | `tenantId: string, dto: CreateSkillTypeDto` | `Promise` | Crea tipo | +| `updateSkillType` | `id: string, tenantId: string, dto: UpdateSkillTypeDto` | `Promise` | Actualiza tipo | +| `deleteSkillType` | `id: string, tenantId: string` | `Promise` | Elimina tipo | + +#### Metodos - Habilidades + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAllSkills` | `tenantId: string, filters: SkillFilters` | `Promise<{ data: Skill[]; total: number; page: number; limit: number }>` | Lista habilidades | +| `findSkillById` | `id: string, tenantId: string` | `Promise` | Obtiene habilidad por ID | +| `createSkill` | `tenantId: string, dto: CreateSkillDto` | `Promise` | Crea habilidad | +| `updateSkill` | `id: string, tenantId: string, dto: UpdateSkillDto` | `Promise` | Actualiza habilidad | +| `deleteSkill` | `id: string, tenantId: string` | `Promise` | Elimina habilidad | + +#### Metodos - Niveles de Habilidad + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAllSkillLevels` | `tenantId: string, filters: SkillLevelFilters` | `Promise<{ data: SkillLevel[]; total: number; page: number; limit: number }>` | Lista niveles | +| `findSkillLevelById` | `id: string, tenantId: string` | `Promise` | Obtiene nivel por ID | +| `createSkillLevel` | `tenantId: string, dto: CreateSkillLevelDto` | `Promise` | Crea nivel | +| `updateSkillLevel` | `id: string, tenantId: string, dto: UpdateSkillLevelDto` | `Promise` | Actualiza nivel | +| `deleteSkillLevel` | `id: string, tenantId: string` | `Promise` | Elimina nivel | + +#### Metodos - Habilidades de Empleado + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAllEmployeeSkills` | `tenantId: string, filters: EmployeeSkillFilters` | `Promise<{ data: EmployeeSkill[]; total: number; page: number; limit: number }>` | Lista habilidades de empleados | +| `findEmployeeSkillById` | `id: string, tenantId: string` | `Promise` | Obtiene por ID | +| `createEmployeeSkill` | `tenantId: string, dto: CreateEmployeeSkillDto` | `Promise` | Asigna habilidad | +| `updateEmployeeSkill` | `id: string, tenantId: string, dto: UpdateEmployeeSkillDto` | `Promise` | Actualiza asignacion | +| `deleteEmployeeSkill` | `id: string, tenantId: string` | `Promise` | Elimina asignacion | +| `getSkillsByEmployee` | `employeeId: string, tenantId: string` | `Promise` | Obtiene habilidades de un empleado | + +--- + +### 6. ExpensesService + +**Archivo:** `expenses.service.ts` + +#### Metodos - Hojas de Gastos + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAllSheets` | `tenantId: string, filters: ExpenseSheetFilters` | `Promise<{ data: ExpenseSheet[]; total: number; page: number; limit: number }>` | Lista hojas | +| `findSheetById` | `id: string, tenantId: string` | `Promise` | Obtiene hoja por ID | +| `createSheet` | `tenantId: string, dto: CreateExpenseSheetDto, userId?: string` | `Promise` | Crea hoja | +| `updateSheet` | `id: string, tenantId: string, dto: UpdateExpenseSheetDto` | `Promise` | Actualiza hoja | +| `submitSheet` | `id: string, tenantId: string` | `Promise` | Envia hoja | +| `approveSheet` | `id: string, tenantId: string, approvedBy: string` | `Promise` | Aprueba hoja | +| `rejectSheet` | `id: string, tenantId: string` | `Promise` | Rechaza hoja | +| `deleteSheet` | `id: string, tenantId: string` | `Promise` | Elimina hoja | + +#### Metodos - Gastos + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAllExpenses` | `tenantId: string, filters: ExpenseFilters` | `Promise<{ data: Expense[]; total: number; page: number; limit: number }>` | Lista gastos | +| `findExpenseById` | `id: string, tenantId: string` | `Promise` | Obtiene gasto por ID | +| `createExpense` | `tenantId: string, dto: CreateExpenseDto, userId?: string` | `Promise` | Crea gasto | +| `updateExpense` | `id: string, tenantId: string, dto: UpdateExpenseDto` | `Promise` | Actualiza gasto | +| `deleteExpense` | `id: string, tenantId: string` | `Promise` | Elimina gasto | +| `recalculateSheetTotals` | `sheetId: string, tenantId: string` | `Promise` | Recalcula totales | + +#### Flujo de Estados + +``` +draft -> submitted -> approved -> posted -> paid + -> rejected +``` + +#### Reglas de Negocio + +- Solo se pueden modificar/eliminar gastos en `draft` +- `total_amount` se calcula como `unit_amount * quantity` +- Los totales de la hoja se recalculan automaticamente + +--- + +### 7. PayslipsService + +**Archivo:** `payslips.service.ts` + +#### Metodos - Estructuras de Nomina + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAllStructures` | `tenantId: string, filters: PayslipStructureFilters` | `Promise<{ data: PayslipStructure[]; total: number; page: number; limit: number }>` | Lista estructuras | +| `findStructureById` | `id: string, tenantId: string` | `Promise` | Obtiene estructura por ID | +| `createStructure` | `tenantId: string, dto: CreatePayslipStructureDto` | `Promise` | Crea estructura | +| `updateStructure` | `id: string, tenantId: string, dto: UpdatePayslipStructureDto` | `Promise` | Actualiza estructura | +| `deleteStructure` | `id: string, tenantId: string` | `Promise` | Elimina estructura | + +#### Metodos - Nominas + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `findAllPayslips` | `tenantId: string, filters: PayslipFilters` | `Promise<{ data: Payslip[]; total: number; page: number; limit: number }>` | Lista nominas | +| `findPayslipById` | `id: string, tenantId: string` | `Promise` | Obtiene nomina por ID | +| `createPayslip` | `tenantId: string, dto: CreatePayslipDto, userId?: string` | `Promise` | Crea nomina | +| `updatePayslip` | `id: string, tenantId: string, dto: UpdatePayslipDto` | `Promise` | Actualiza nomina | +| `verifyPayslip` | `id: string, tenantId: string` | `Promise` | Verifica nomina | +| `confirmPayslip` | `id: string, tenantId: string` | `Promise` | Confirma nomina | +| `cancelPayslip` | `id: string, tenantId: string` | `Promise` | Cancela nomina | +| `deletePayslip` | `id: string, tenantId: string` | `Promise` | Elimina nomina | + +#### Metodos - Lineas de Nomina + +| Metodo | Parametros | Retorno | Descripcion | +|--------|------------|---------|-------------| +| `getPayslipLines` | `payslipId: string, tenantId: string` | `Promise` | Obtiene lineas | +| `addPayslipLine` | `payslipId: string, tenantId: string, dto: CreatePayslipLineDto` | `Promise` | Agrega linea | +| `updatePayslipLine` | `payslipId: string, lineId: string, tenantId: string, dto: UpdatePayslipLineDto` | `Promise` | Actualiza linea | +| `removePayslipLine` | `payslipId: string, lineId: string, tenantId: string` | `Promise` | Elimina linea | + +#### Flujo de Estados + +``` +draft -> verify -> done +draft/verify -> cancel +``` + +#### Reglas de Negocio + +- Solo se pueden modificar nominas en `draft` +- Solo se pueden eliminar nominas en `draft` o `cancel` +- Los totales se recalculan al modificar lineas +- El numero de nomina se genera al confirmar + +--- + +## ENTIDADES + +### Employee + +```typescript +interface Employee { + id: string; + tenant_id: string; + company_id: string; + company_name?: string; + employee_number: string; + first_name: string; + last_name: string; + middle_name?: string; + full_name?: string; + user_id?: string; + birth_date?: Date; + gender?: string; + marital_status?: string; + nationality?: string; + identification_id?: string; + identification_type?: string; + social_security_number?: string; + tax_id?: string; + email?: string; + work_email?: string; + phone?: string; + work_phone?: string; + mobile?: string; + emergency_contact?: string; + emergency_phone?: string; + street?: string; + city?: string; + state?: string; + zip?: string; + country?: string; + department_id?: string; + department_name?: string; + job_position_id?: string; + job_position_name?: string; + manager_id?: string; + manager_name?: string; + hire_date: Date; + termination_date?: Date; + status: EmployeeStatus; + bank_name?: string; + bank_account?: string; + bank_clabe?: string; + photo_url?: string; + notes?: string; + created_at: Date; +} + +type EmployeeStatus = 'active' | 'inactive' | 'on_leave' | 'terminated'; +``` + +### Department + +```typescript +interface Department { + id: string; + tenant_id: string; + company_id: string; + company_name?: string; + name: string; + code?: string; + parent_id?: string; + parent_name?: string; + manager_id?: string; + manager_name?: string; + description?: string; + color?: string; + active: boolean; + employee_count?: number; + created_at: Date; +} +``` + +### JobPosition + +```typescript +interface JobPosition { + id: string; + tenant_id: string; + name: string; + department_id?: string; + department_name?: string; + description?: string; + requirements?: string; + responsibilities?: string; + min_salary?: number; + max_salary?: number; + active: boolean; + employee_count?: number; + created_at: Date; +} +``` + +### Contract + +```typescript +interface Contract { + id: string; + tenant_id: string; + company_id: string; + company_name?: string; + employee_id: string; + employee_name?: string; + employee_number?: string; + name: string; + reference?: string; + contract_type: ContractType; + status: ContractStatus; + job_position_id?: string; + job_position_name?: string; + department_id?: string; + department_name?: string; + date_start: Date; + date_end?: Date; + trial_date_end?: Date; + wage: number; + wage_type: string; + currency_id?: string; + currency_code?: string; + hours_per_week: number; + vacation_days: number; + christmas_bonus_days: number; + document_url?: string; + notes?: string; + created_at: Date; +} + +type ContractStatus = 'draft' | 'active' | 'expired' | 'terminated' | 'cancelled'; +type ContractType = 'permanent' | 'temporary' | 'contractor' | 'internship' | 'part_time'; +``` + +### Leave + +```typescript +interface Leave { + id: string; + tenant_id: string; + company_id: string; + company_name?: string; + employee_id: string; + employee_name?: string; + employee_number?: string; + leave_type_id: string; + leave_type_name?: string; + name?: string; + date_from: Date; + date_to: Date; + number_of_days: number; + status: LeaveStatus; + description?: string; + approved_by?: string; + approved_by_name?: string; + approved_at?: Date; + rejection_reason?: string; + created_at: Date; +} + +type LeaveStatus = 'draft' | 'submitted' | 'approved' | 'rejected' | 'cancelled'; +type LeaveType = 'vacation' | 'sick' | 'personal' | 'maternity' | 'paternity' | 'bereavement' | 'unpaid' | 'other'; +``` + +### LeaveTypeConfig + +```typescript +interface LeaveTypeConfig { + id: string; + tenant_id: string; + name: string; + code?: string; + leave_type: LeaveType; + requires_approval: boolean; + max_days?: number; + is_paid: boolean; + color?: string; + active: boolean; + created_at: Date; +} +``` + +### Skill, SkillType, SkillLevel, EmployeeSkill + +```typescript +interface SkillType { + id: string; + tenant_id: string; + name: string; + skill_levels: string; + created_at: Date; +} + +interface Skill { + id: string; + tenant_id: string; + skill_type_id: string; + skill_type_name?: string; + name: string; + created_at: Date; +} + +interface SkillLevel { + id: string; + tenant_id: string; + skill_type_id: string; + skill_type_name?: string; + name: string; + level: number; + created_at: Date; +} + +interface EmployeeSkill { + id: string; + employee_id: string; + employee_name?: string; + skill_id: string; + skill_name?: string; + skill_level_id?: string; + skill_level_name?: string; + skill_type_id?: string; + skill_type_name?: string; + created_at: Date; +} +``` + +### ExpenseSheet, Expense + +```typescript +interface ExpenseSheet { + id: string; + tenant_id: string; + company_id: string; + employee_id: string; + employee_name?: string; + name: string; + state: ExpenseStatus; + total_amount: number; + untaxed_amount: number; + total_amount_taxes: number; + journal_id?: string; + account_move_id?: string; + user_id?: string; + user_name?: string; + approved_by?: string; + approved_by_name?: string; + approved_date?: Date; + accounting_date?: Date; + created_at: Date; + created_by?: string; + updated_at?: Date; +} + +interface Expense { + id: string; + tenant_id: string; + company_id: string; + employee_id: string; + employee_name?: string; + name: string; + sheet_id?: string; + sheet_name?: string; + product_id?: string; + product_name?: string; + unit_amount: number; + quantity: number; + total_amount: number; + untaxed_amount?: number; + total_amount_taxes?: number; + currency_id?: string; + currency_code?: string; + tax_ids?: string[]; + date: Date; + description?: string; + reference?: string; + analytic_account_id?: string; + analytic_account_name?: string; + state: ExpenseStatus; + payment_mode: string; + created_at: Date; + created_by?: string; + updated_at?: Date; +} + +type ExpenseStatus = 'draft' | 'submitted' | 'approved' | 'posted' | 'paid' | 'rejected'; +``` + +### Payslip, PayslipStructure, PayslipLine + +```typescript +interface PayslipStructure { + id: string; + tenant_id: string; + name: string; + code?: string; + is_active: boolean; + note?: string; + created_at: Date; +} + +interface Payslip { + id: string; + tenant_id: string; + company_id: string; + employee_id: string; + employee_name?: string; + employee_number?: string; + contract_id?: string; + contract_name?: string; + name: string; + number?: string; + state: PayslipStatus; + date_from: Date; + date_to: Date; + date?: Date; + structure_id?: string; + structure_name?: string; + basic_wage?: number; + gross_wage?: number; + net_wage?: number; + worked_days?: number; + worked_hours?: number; + journal_id?: string; + move_id?: string; + created_at: Date; + created_by?: string; + updated_at?: Date; +} + +interface PayslipLine { + id: string; + payslip_id: string; + name: string; + code: string; + sequence: number; + category?: string; + quantity: number; + rate: number; + amount: number; + total: number; + appears_on_payslip: boolean; + created_at: Date; +} + +type PayslipStatus = 'draft' | 'verify' | 'done' | 'cancel'; +``` + +--- + +## DTOs + +### Employees DTOs + +```typescript +interface CreateEmployeeDto { + company_id: string; + employee_number: string; + first_name: string; + last_name: string; + middle_name?: string; + user_id?: string; + birth_date?: string; + gender?: string; + marital_status?: string; + nationality?: string; + identification_id?: string; + identification_type?: string; + social_security_number?: string; + tax_id?: string; + email?: string; + work_email?: string; + phone?: string; + work_phone?: string; + mobile?: string; + emergency_contact?: string; + emergency_phone?: string; + street?: string; + city?: string; + state?: string; + zip?: string; + country?: string; + department_id?: string; + job_position_id?: string; + manager_id?: string; + hire_date: string; + bank_name?: string; + bank_account?: string; + bank_clabe?: string; + photo_url?: string; + notes?: string; +} + +interface UpdateEmployeeDto { + first_name?: string; + last_name?: string; + middle_name?: string | null; + user_id?: string | null; + birth_date?: string | null; + gender?: string | null; + marital_status?: string | null; + nationality?: string | null; + identification_id?: string | null; + identification_type?: string | null; + social_security_number?: string | null; + tax_id?: string | null; + email?: string | null; + work_email?: string | null; + phone?: string | null; + work_phone?: string | null; + mobile?: string | null; + emergency_contact?: string | null; + emergency_phone?: string | null; + street?: string | null; + city?: string | null; + state?: string | null; + zip?: string | null; + country?: string | null; + department_id?: string | null; + job_position_id?: string | null; + manager_id?: string | null; + bank_name?: string | null; + bank_account?: string | null; + bank_clabe?: string | null; + photo_url?: string | null; + notes?: string | null; +} + +interface EmployeeFilters { + company_id?: string; + department_id?: string; + status?: EmployeeStatus; + manager_id?: string; + search?: string; + page?: number; + limit?: number; +} +``` + +### Departments DTOs + +```typescript +interface CreateDepartmentDto { + company_id: string; + name: string; + code?: string; + parent_id?: string; + manager_id?: string; + description?: string; + color?: string; +} + +interface UpdateDepartmentDto { + name?: string; + code?: string | null; + parent_id?: string | null; + manager_id?: string | null; + description?: string | null; + color?: string | null; + active?: boolean; +} + +interface DepartmentFilters { + company_id?: string; + active?: boolean; + search?: string; + page?: number; + limit?: number; +} + +interface CreateJobPositionDto { + name: string; + department_id?: string; + description?: string; + requirements?: string; + responsibilities?: string; + min_salary?: number; + max_salary?: number; +} + +interface UpdateJobPositionDto { + name?: string; + department_id?: string | null; + description?: string | null; + requirements?: string | null; + responsibilities?: string | null; + min_salary?: number | null; + max_salary?: number | null; + active?: boolean; +} +``` + +### Contracts DTOs + +```typescript +interface CreateContractDto { + company_id: string; + employee_id: string; + name: string; + reference?: string; + contract_type: ContractType; + job_position_id?: string; + department_id?: string; + date_start: string; + date_end?: string; + trial_date_end?: string; + wage: number; + wage_type?: string; + currency_id?: string; + hours_per_week?: number; + vacation_days?: number; + christmas_bonus_days?: number; + document_url?: string; + notes?: string; +} + +interface UpdateContractDto { + reference?: string | null; + job_position_id?: string | null; + department_id?: string | null; + date_end?: string | null; + trial_date_end?: string | null; + wage?: number; + wage_type?: string; + currency_id?: string | null; + hours_per_week?: number; + vacation_days?: number; + christmas_bonus_days?: number; + document_url?: string | null; + notes?: string | null; +} + +interface ContractFilters { + company_id?: string; + employee_id?: string; + status?: ContractStatus; + contract_type?: ContractType; + search?: string; + page?: number; + limit?: number; +} +``` + +### Leaves DTOs + +```typescript +interface CreateLeaveTypeDto { + name: string; + code?: string; + leave_type: LeaveType; + requires_approval?: boolean; + max_days?: number; + is_paid?: boolean; + color?: string; +} + +interface UpdateLeaveTypeDto { + name?: string; + code?: string | null; + requires_approval?: boolean; + max_days?: number | null; + is_paid?: boolean; + color?: string | null; + active?: boolean; +} + +interface CreateLeaveDto { + company_id: string; + employee_id: string; + leave_type_id: string; + name?: string; + date_from: string; + date_to: string; + description?: string; +} + +interface UpdateLeaveDto { + leave_type_id?: string; + name?: string | null; + date_from?: string; + date_to?: string; + description?: string | null; +} + +interface LeaveFilters { + company_id?: string; + employee_id?: string; + leave_type_id?: string; + status?: LeaveStatus; + date_from?: string; + date_to?: string; + search?: string; + page?: number; + limit?: number; +} +``` + +### Skills DTOs + +```typescript +interface CreateSkillTypeDto { + name: string; + skill_levels?: string; +} + +interface UpdateSkillTypeDto { + name?: string; + skill_levels?: string; +} + +interface CreateSkillDto { + skill_type_id: string; + name: string; +} + +interface UpdateSkillDto { + name?: string; + skill_type_id?: string; +} + +interface CreateSkillLevelDto { + skill_type_id: string; + name: string; + level: number; +} + +interface UpdateSkillLevelDto { + name?: string; + level?: number; +} + +interface CreateEmployeeSkillDto { + employee_id: string; + skill_id: string; + skill_level_id?: string; + skill_type_id?: string; +} + +interface UpdateEmployeeSkillDto { + skill_level_id?: string | null; + skill_type_id?: string | null; +} +``` + +### Expenses DTOs + +```typescript +interface CreateExpenseSheetDto { + company_id: string; + employee_id: string; + name: string; + user_id?: string; + accounting_date?: string; +} + +interface UpdateExpenseSheetDto { + name?: string; + user_id?: string | null; + accounting_date?: string | null; +} + +interface CreateExpenseDto { + company_id: string; + employee_id: string; + name: string; + sheet_id?: string; + product_id?: string; + unit_amount: number; + quantity?: number; + currency_id?: string; + tax_ids?: string[]; + date?: string; + description?: string; + reference?: string; + analytic_account_id?: string; + payment_mode?: string; +} + +interface UpdateExpenseDto { + name?: string; + sheet_id?: string | null; + product_id?: string | null; + unit_amount?: number; + quantity?: number; + currency_id?: string | null; + tax_ids?: string[]; + date?: string; + description?: string | null; + reference?: string | null; + analytic_account_id?: string | null; + payment_mode?: string; +} + +interface ExpenseSheetFilters { + company_id?: string; + employee_id?: string; + state?: ExpenseStatus; + date_from?: string; + date_to?: string; + search?: string; + page?: number; + limit?: number; +} + +interface ExpenseFilters { + company_id?: string; + employee_id?: string; + sheet_id?: string; + state?: ExpenseStatus; + date_from?: string; + date_to?: string; + search?: string; + page?: number; + limit?: number; +} +``` + +### Payslips DTOs + +```typescript +interface CreatePayslipStructureDto { + name: string; + code?: string; + is_active?: boolean; + note?: string; +} + +interface UpdatePayslipStructureDto { + name?: string; + code?: string | null; + is_active?: boolean; + note?: string | null; +} + +interface CreatePayslipDto { + company_id: string; + employee_id: string; + contract_id?: string; + name: string; + date_from: string; + date_to: string; + date?: string; + structure_id?: string; + basic_wage?: number; + worked_days?: number; + worked_hours?: number; +} + +interface UpdatePayslipDto { + name?: string; + date?: string | null; + structure_id?: string | null; + basic_wage?: number; + gross_wage?: number; + net_wage?: number; + worked_days?: number; + worked_hours?: number; +} + +interface CreatePayslipLineDto { + name: string; + code: string; + sequence?: number; + category?: string; + quantity?: number; + rate?: number; + amount: number; + appears_on_payslip?: boolean; +} + +interface UpdatePayslipLineDto { + name?: string; + code?: string; + sequence?: number; + category?: string; + quantity?: number; + rate?: number; + amount?: number; + appears_on_payslip?: boolean; +} + +interface PayslipStructureFilters { + is_active?: boolean; + search?: string; + page?: number; + limit?: number; +} + +interface PayslipFilters { + company_id?: string; + employee_id?: string; + contract_id?: string; + structure_id?: string; + state?: PayslipStatus; + date_from?: string; + date_to?: string; + search?: string; + page?: number; + limit?: number; +} +``` + +--- + +## ENDPOINTS + +### Employees + +| Metodo | Ruta | Descripcion | +|--------|------|-------------| +| GET | `/api/hr/employees` | Listar empleados | +| GET | `/api/hr/employees/:id` | Obtener empleado | +| POST | `/api/hr/employees` | Crear empleado | +| PUT | `/api/hr/employees/:id` | Actualizar empleado | +| DELETE | `/api/hr/employees/:id` | Eliminar empleado | +| POST | `/api/hr/employees/:id/terminate` | Dar de baja | +| POST | `/api/hr/employees/:id/reactivate` | Reactivar | +| GET | `/api/hr/employees/:id/subordinates` | Obtener subordinados | + +### Departments + +| Metodo | Ruta | Descripcion | +|--------|------|-------------| +| GET | `/api/hr/departments` | Listar departamentos | +| GET | `/api/hr/departments/:id` | Obtener departamento | +| POST | `/api/hr/departments` | Crear departamento | +| PUT | `/api/hr/departments/:id` | Actualizar departamento | +| DELETE | `/api/hr/departments/:id` | Eliminar departamento | + +### Job Positions + +| Metodo | Ruta | Descripcion | +|--------|------|-------------| +| GET | `/api/hr/job-positions` | Listar puestos | +| GET | `/api/hr/job-positions/:id` | Obtener puesto | +| POST | `/api/hr/job-positions` | Crear puesto | +| PUT | `/api/hr/job-positions/:id` | Actualizar puesto | +| DELETE | `/api/hr/job-positions/:id` | Eliminar puesto | + +### Contracts + +| Metodo | Ruta | Descripcion | +|--------|------|-------------| +| GET | `/api/hr/contracts` | Listar contratos | +| GET | `/api/hr/contracts/:id` | Obtener contrato | +| POST | `/api/hr/contracts` | Crear contrato | +| PUT | `/api/hr/contracts/:id` | Actualizar contrato | +| DELETE | `/api/hr/contracts/:id` | Eliminar contrato | +| POST | `/api/hr/contracts/:id/activate` | Activar contrato | +| POST | `/api/hr/contracts/:id/terminate` | Terminar contrato | +| POST | `/api/hr/contracts/:id/cancel` | Cancelar contrato | + +### Leaves + +| Metodo | Ruta | Descripcion | +|--------|------|-------------| +| GET | `/api/hr/leave-types` | Listar tipos de ausencia | +| GET | `/api/hr/leave-types/:id` | Obtener tipo | +| POST | `/api/hr/leave-types` | Crear tipo | +| PUT | `/api/hr/leave-types/:id` | Actualizar tipo | +| DELETE | `/api/hr/leave-types/:id` | Eliminar tipo | +| GET | `/api/hr/leaves` | Listar ausencias | +| GET | `/api/hr/leaves/:id` | Obtener ausencia | +| POST | `/api/hr/leaves` | Crear ausencia | +| PUT | `/api/hr/leaves/:id` | Actualizar ausencia | +| DELETE | `/api/hr/leaves/:id` | Eliminar ausencia | +| POST | `/api/hr/leaves/:id/submit` | Enviar solicitud | +| POST | `/api/hr/leaves/:id/approve` | Aprobar solicitud | +| POST | `/api/hr/leaves/:id/reject` | Rechazar solicitud | +| POST | `/api/hr/leaves/:id/cancel` | Cancelar solicitud | + +### Skills + +| Metodo | Ruta | Descripcion | +|--------|------|-------------| +| GET | `/api/hr/skill-types` | Listar tipos de habilidad | +| GET | `/api/hr/skill-types/:id` | Obtener tipo | +| POST | `/api/hr/skill-types` | Crear tipo | +| PUT | `/api/hr/skill-types/:id` | Actualizar tipo | +| DELETE | `/api/hr/skill-types/:id` | Eliminar tipo | +| GET | `/api/hr/skills` | Listar habilidades | +| GET | `/api/hr/skills/:id` | Obtener habilidad | +| POST | `/api/hr/skills` | Crear habilidad | +| PUT | `/api/hr/skills/:id` | Actualizar habilidad | +| DELETE | `/api/hr/skills/:id` | Eliminar habilidad | +| GET | `/api/hr/skill-levels` | Listar niveles | +| GET | `/api/hr/skill-levels/:id` | Obtener nivel | +| POST | `/api/hr/skill-levels` | Crear nivel | +| PUT | `/api/hr/skill-levels/:id` | Actualizar nivel | +| DELETE | `/api/hr/skill-levels/:id` | Eliminar nivel | +| GET | `/api/hr/employee-skills` | Listar habilidades de empleados | +| GET | `/api/hr/employee-skills/:id` | Obtener asignacion | +| POST | `/api/hr/employee-skills` | Asignar habilidad | +| PUT | `/api/hr/employee-skills/:id` | Actualizar asignacion | +| DELETE | `/api/hr/employee-skills/:id` | Eliminar asignacion | +| GET | `/api/hr/employees/:id/skills` | Habilidades de empleado | + +### Expenses + +| Metodo | Ruta | Descripcion | +|--------|------|-------------| +| GET | `/api/hr/expense-sheets` | Listar hojas de gastos | +| GET | `/api/hr/expense-sheets/:id` | Obtener hoja | +| POST | `/api/hr/expense-sheets` | Crear hoja | +| PUT | `/api/hr/expense-sheets/:id` | Actualizar hoja | +| DELETE | `/api/hr/expense-sheets/:id` | Eliminar hoja | +| POST | `/api/hr/expense-sheets/:id/submit` | Enviar hoja | +| POST | `/api/hr/expense-sheets/:id/approve` | Aprobar hoja | +| POST | `/api/hr/expense-sheets/:id/reject` | Rechazar hoja | +| GET | `/api/hr/expenses` | Listar gastos | +| GET | `/api/hr/expenses/:id` | Obtener gasto | +| POST | `/api/hr/expenses` | Crear gasto | +| PUT | `/api/hr/expenses/:id` | Actualizar gasto | +| DELETE | `/api/hr/expenses/:id` | Eliminar gasto | + +### Payslips + +| Metodo | Ruta | Descripcion | +|--------|------|-------------| +| GET | `/api/hr/payslip-structures` | Listar estructuras | +| GET | `/api/hr/payslip-structures/:id` | Obtener estructura | +| POST | `/api/hr/payslip-structures` | Crear estructura | +| PUT | `/api/hr/payslip-structures/:id` | Actualizar estructura | +| DELETE | `/api/hr/payslip-structures/:id` | Eliminar estructura | +| GET | `/api/hr/payslips` | Listar nominas | +| GET | `/api/hr/payslips/:id` | Obtener nomina | +| POST | `/api/hr/payslips` | Crear nomina | +| PUT | `/api/hr/payslips/:id` | Actualizar nomina | +| DELETE | `/api/hr/payslips/:id` | Eliminar nomina | +| POST | `/api/hr/payslips/:id/verify` | Verificar nomina | +| POST | `/api/hr/payslips/:id/confirm` | Confirmar nomina | +| POST | `/api/hr/payslips/:id/cancel` | Cancelar nomina | +| GET | `/api/hr/payslips/:id/lines` | Obtener lineas | +| POST | `/api/hr/payslips/:id/lines` | Agregar linea | +| PUT | `/api/hr/payslips/:id/lines/:lineId` | Actualizar linea | +| DELETE | `/api/hr/payslips/:id/lines/:lineId` | Eliminar linea | + +--- + +## TESTS + +### Ubicacion de Tests + +``` +backend/src/modules/hr/__tests__/ + employees.test.ts + departments.test.ts + contracts.test.ts + leaves.test.ts + skills.test.ts + expenses.test.ts + payslips.test.ts +``` + +### Casos de Prueba Requeridos + +#### EmployeesService + +- [ ] Crear empleado con datos validos +- [ ] Error al crear empleado con numero duplicado +- [ ] Obtener empleado existente +- [ ] Error al obtener empleado inexistente +- [ ] Listar empleados con filtros +- [ ] Actualizar empleado +- [ ] Dar de baja empleado +- [ ] Error al dar de baja empleado ya dado de baja +- [ ] Reactivar empleado +- [ ] Error al eliminar empleado con contratos +- [ ] Error al eliminar empleado que es manager +- [ ] Obtener subordinados + +#### DepartmentsService + +- [ ] Crear departamento +- [ ] Error al crear departamento con nombre duplicado +- [ ] Listar departamentos +- [ ] Actualizar departamento +- [ ] Error al eliminar departamento con empleados +- [ ] Error al eliminar departamento con subdepartamentos +- [ ] CRUD de puestos de trabajo + +#### ContractsService + +- [ ] Crear contrato +- [ ] Error al crear contrato con empleado que ya tiene uno activo +- [ ] Activar contrato +- [ ] Terminar contrato +- [ ] Cancelar contrato +- [ ] Error al editar contrato no borrador + +#### LeavesService + +- [ ] Crear tipo de ausencia +- [ ] Crear solicitud de ausencia +- [ ] Validar dias maximos por tipo +- [ ] Error al crear ausencia solapada +- [ ] Flujo completo: draft -> submitted -> approved +- [ ] Rechazar solicitud con razon +- [ ] Cancelar solicitud + +#### SkillsService + +- [ ] CRUD de tipos de habilidad +- [ ] CRUD de habilidades +- [ ] CRUD de niveles +- [ ] Asignar habilidad a empleado +- [ ] Obtener habilidades de empleado + +#### ExpensesService + +- [ ] Crear hoja de gastos +- [ ] Agregar gastos a hoja +- [ ] Recalculo automatico de totales +- [ ] Flujo de aprobacion +- [ ] Error al modificar gasto no borrador + +#### PayslipsService + +- [ ] Crear estructura de nomina +- [ ] Crear nomina +- [ ] Agregar lineas a nomina +- [ ] Recalculo de gross/net +- [ ] Flujo: draft -> verify -> done +- [ ] Cancelar nomina +- [ ] Error al modificar nomina confirmada + +--- + +## DEPENDENCIAS + +### Internas + +| Modulo | Descripcion | +|--------|-------------| +| `auth` | Empresas y usuarios | +| `core` | Monedas | +| `inventory` | Productos (para gastos) | +| `analytics` | Cuentas analiticas | + +### Tablas Referenciadas + +- `auth.companies` - Empresa del empleado/contrato +- `auth.users` - Usuario asociado al empleado +- `core.currencies` - Moneda del contrato/gasto +- `inventory.products` - Producto del gasto +- `analytics.analytic_accounts` - Cuenta analitica + +### Externas + +```json +{ + "pg": "Base de datos PostgreSQL" +} +``` + +--- + +## ERRORES COMUNES + +| Codigo | Mensaje | Causa | +|--------|---------|-------| +| `NOT_FOUND` | Empleado no encontrado | ID invalido o no pertenece al tenant | +| `CONFLICT` | Ya existe un empleado con ese numero | `employee_number` duplicado | +| `CONFLICT` | No se puede eliminar un empleado con contratos | Tiene contratos asociados | +| `CONFLICT` | No se puede eliminar un empleado que es manager | Es manager de otros empleados | +| `VALIDATION` | El empleado ya tiene un contrato activo | Intento de crear segundo contrato activo | +| `VALIDATION` | Solo se pueden editar contratos en estado borrador | Contrato no esta en `draft` | +| `VALIDATION` | Ya existe una solicitud de ausencia para estas fechas | Ausencias solapadas | +| `VALIDATION` | Este tipo de ausencia tiene un maximo de X dias | Excede dias permitidos | +| `VALIDATION` | Solo se pueden modificar gastos en estado borrador | Gasto no esta en `draft` | +| `VALIDATION` | Solo se pueden modificar nominas en estado borrador | Nomina no esta en `draft` | + +--- + +## CHANGELOG + +| Version | Fecha | Cambios | +|---------|-------|---------| +| 1.0.0 | 2026-01-10 | Especificacion inicial con 7 servicios | diff --git a/docs/03-fase-vertical/MGN-015-crm/README.md b/docs/03-fase-vertical/MGN-015-crm/README.md new file mode 100644 index 0000000..77acb4e --- /dev/null +++ b/docs/03-fase-vertical/MGN-015-crm/README.md @@ -0,0 +1,48 @@ +# MGN-015: CRM (Customer Relationship Management) + +## Descripción + +Módulo de gestión de relaciones con clientes, incluyendo leads, oportunidades de venta y pipeline comercial. + +## Implementación Backend + +**Ubicación:** `/backend/src/modules/crm/` + +### Servicios Implementados (4) + +| Servicio | Descripción | +|----------|-------------| +| `leads.service.ts` | Prospectos/Leads | +| `opportunities.service.ts` | Oportunidades de venta | +| `stages.service.ts` | Etapas del pipeline | +| `tags.service.ts` | Etiquetas para clasificación | + +### Funcionalidades + +- Gestión de leads +- Pipeline de oportunidades +- Probabilidad de ganancia +- Fuentes de leads (website, phone, email, referral, social media) +- Razones de pérdida +- Tags para segmentación +- Seguimiento de equipos de ventas + +## Dependencias + +- **MGN-017 (Partners):** Clientes potenciales +- **MGN-011 (Sales):** Conversión a ventas + +## Estado de Documentación + +| Artefacto | Estado | +|-----------|--------| +| README | Básico | +| Requerimientos Funcionales | Pendiente | +| Especificaciones Técnicas | Pendiente | +| User Stories | Pendiente | + +**Complejidad:** MEDIA + +--- + +*Módulo identificado durante sincronización docs-código: 2026-01-10* diff --git a/docs/03-fase-vertical/MGN-016-projects/README.md b/docs/03-fase-vertical/MGN-016-projects/README.md new file mode 100644 index 0000000..3fdddbf --- /dev/null +++ b/docs/03-fase-vertical/MGN-016-projects/README.md @@ -0,0 +1,54 @@ +# MGN-016: Projects (Gestión de Proyectos) + +## Descripción + +Módulo de gestión de proyectos, tareas y hojas de tiempo para tracking de trabajo. + +## Implementación Backend + +**Ubicación:** `/backend/src/modules/projects/` + +### Servicios Implementados (3) + +| Servicio | Descripción | +|----------|-------------| +| `projects.service.ts` | Proyectos | +| `tasks.service.ts` | Tareas | +| `timesheets.service.ts` | Hojas de tiempo | + +### Estados de Proyectos + +- `draft` - Borrador +- `active` - Activo +- `completed` - Completado +- `cancelled` - Cancelado +- `on_hold` - En espera + +### Funcionalidades + +- Gestión de proyectos con fechas +- Asignación de responsables +- Control de privacidad +- Tareas con subtareas +- Prioridades y fechas límite +- Hojas de tiempo + +## Dependencias + +- **MGN-017 (Partners):** Clientes de proyecto +- **MGN-014 (HR):** Empleados asignados + +## Estado de Documentación + +| Artefacto | Estado | +|-----------|--------| +| README | Básico | +| Requerimientos Funcionales | Pendiente | +| Especificaciones Técnicas | Pendiente | +| User Stories | Pendiente | + +**Complejidad:** MEDIA-ALTA + +--- + +*Módulo identificado durante sincronización docs-código: 2026-01-10* diff --git a/docs/03-fase-vertical/MGN-017-partners/README.md b/docs/03-fase-vertical/MGN-017-partners/README.md new file mode 100644 index 0000000..aa74201 --- /dev/null +++ b/docs/03-fase-vertical/MGN-017-partners/README.md @@ -0,0 +1,57 @@ +# MGN-017: Partners (Gestión de Socios Comerciales) + +## Descripción + +Módulo centralizado de gestión de clientes, proveedores y otros socios comerciales. + +## Implementación Backend + +**Ubicación:** `/backend/src/modules/partners/` + +### Servicios Implementados (2) + +| Servicio | Descripción | +|----------|-------------| +| `partners.service.ts` | Clientes y proveedores | +| `ranking.service.ts` | Evaluación/Ranking de socios | + +### Tipos de Partner + +- `person` - Persona física +- `company` - Empresa + +### Roles + +- Cliente +- Proveedor +- Empleado +- Empresa + +### Funcionalidades + +- Gestión centralizada +- Información de contacto +- Datos bancarios +- Moneda de operación +- Jerarquía de partners +- Ranking de proveedores +- Clasificación multinivel + +## Dependencias + +- **MGN-004 (Tenants):** Aislamiento multi-tenant + +## Estado de Documentación + +| Artefacto | Estado | +|-----------|--------| +| README | Básico | +| Requerimientos Funcionales | Pendiente | +| Especificaciones Técnicas | Pendiente | +| User Stories | Pendiente | + +**Complejidad:** MEDIA + +--- + +*Módulo identificado durante sincronización docs-código: 2026-01-10* diff --git a/docs/03-fase-vertical/MGN-018-companies/README.md b/docs/03-fase-vertical/MGN-018-companies/README.md new file mode 100644 index 0000000..e0c4bba --- /dev/null +++ b/docs/03-fase-vertical/MGN-018-companies/README.md @@ -0,0 +1,43 @@ +# MGN-018: Companies (Gestión de Empresas) + +## Descripción + +Módulo de gestión multi-empresa para soporte de filiales y estructura corporativa. + +## Implementación Backend + +**Ubicación:** `/backend/src/modules/companies/` + +### Servicios Implementados (1) + +| Servicio | Descripción | +|----------|-------------| +| `companies.service.ts` | Empresas/entidades | + +### Funcionalidades + +- Gestión de empresas filiales +- Información legal y fiscal +- Moneda operacional +- Jerarquía de empresas +- Configuraciones por empresa +- Soporte multi-empresa + +## Dependencias + +- **MGN-004 (Tenants):** Aislamiento multi-tenant + +## Estado de Documentación + +| Artefacto | Estado | +|-----------|--------| +| README | Básico | +| Requerimientos Funcionales | Pendiente | +| Especificaciones Técnicas | Pendiente | +| User Stories | Pendiente | + +**Complejidad:** BAJA + +--- + +*Módulo identificado durante sincronización docs-código: 2026-01-10* diff --git a/docs/03-fase-vertical/README.md b/docs/03-fase-vertical/README.md new file mode 100644 index 0000000..517e5cc --- /dev/null +++ b/docs/03-fase-vertical/README.md @@ -0,0 +1,55 @@ +# Fase 3: Módulos Verticales de Negocio + +## Descripción + +Esta fase contiene la documentación de los módulos verticales de negocio que extienden las funcionalidades base del ERP. + +## Módulos Incluidos + +| MGN-ID | Módulo | Descripción | Complejidad | +|--------|--------|-------------|-------------| +| MGN-011 | Sales | Gestión de ventas, cotizaciones y órdenes | Alta | +| MGN-012 | Purchases | Gestión de compras y RFQ | Media | +| MGN-013 | Inventory | Gestión de inventario, almacenes y valuación | Muy Alta | +| MGN-014 | HR | Recursos humanos, nómina y empleados | Muy Alta | +| MGN-015 | CRM | Gestión de leads y oportunidades | Media | +| MGN-016 | Projects | Gestión de proyectos y tareas | Media-Alta | +| MGN-017 | Partners | Gestión de socios comerciales | Media | +| MGN-018 | Companies | Gestión multi-empresa | Baja | + +## Estado de Documentación + +| Módulo | README | RF | ET | US | Estado | +|--------|--------|----|----|----|----| +| MGN-011 | Básico | Pendiente | Pendiente | Pendiente | En Desarrollo | +| MGN-012 | Básico | Pendiente | Pendiente | Pendiente | En Desarrollo | +| MGN-013 | Básico | Pendiente | Pendiente | Pendiente | En Desarrollo | +| MGN-014 | Básico | Pendiente | Pendiente | Pendiente | En Desarrollo | +| MGN-015 | Básico | Pendiente | Pendiente | Pendiente | En Desarrollo | +| MGN-016 | Básico | Pendiente | Pendiente | Pendiente | En Desarrollo | +| MGN-017 | Básico | Pendiente | Pendiente | Pendiente | En Desarrollo | +| MGN-018 | Básico | Pendiente | Pendiente | Pendiente | En Desarrollo | + +## Dependencias + +``` +MGN-011 (Sales) ← MGN-017 (Partners), MGN-013 (Inventory), MGN-010 (Financial) +MGN-012 (Purchases) ← MGN-017 (Partners), MGN-013 (Inventory), MGN-010 (Financial) +MGN-013 (Inventory) ← MGN-005 (Catalogs) +MGN-014 (HR) ← MGN-018 (Companies) +MGN-015 (CRM) ← MGN-017 (Partners), MGN-011 (Sales) +MGN-016 (Projects) ← MGN-017 (Partners), MGN-014 (HR) +MGN-017 (Partners) ← MGN-004 (Tenants) +MGN-018 (Companies) ← MGN-004 (Tenants) +``` + +## Prioridad de Documentación + +1. **Crítica:** MGN-011 (Sales), MGN-012 (Purchases), MGN-013 (Inventory) +2. **Alta:** MGN-014 (HR), MGN-017 (Partners) +3. **Media:** MGN-015 (CRM), MGN-016 (Projects) +4. **Baja:** MGN-018 (Companies) + +--- + +*Generado durante reestructuración de documentación: 2026-01-10* diff --git a/docs/03-requerimientos/RF-auth/INDICE-RF-AUTH.md b/docs/03-requerimientos/RF-auth/INDICE-RF-AUTH.md deleted file mode 100644 index a86dfb8..0000000 --- a/docs/03-requerimientos/RF-auth/INDICE-RF-AUTH.md +++ /dev/null @@ -1,188 +0,0 @@ -# Indice de Requerimientos Funcionales - MGN-001 Auth - -## Resumen del Modulo - -| Campo | Valor | -|-------|-------| -| **Codigo** | MGN-001 | -| **Nombre** | Auth - Autenticacion | -| **Prioridad** | P0 - Critica | -| **Total RFs** | 5 | -| **Estado** | En documentacion | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion General - -El modulo de autenticacion es el pilar fundamental de seguridad del ERP. Proporciona: - -- **Autenticacion**: Verificacion de identidad del usuario -- **Autorizacion**: Generacion de tokens con permisos -- **Gestion de Sesiones**: Control del ciclo de vida de sesiones -- **Seguridad**: Proteccion contra ataques comunes - ---- - -## Lista de Requerimientos Funcionales - -| ID | Nombre | Prioridad | Complejidad | Estado | Story Points | -|----|--------|-----------|-------------|--------|--------------| -| [RF-AUTH-001](./RF-AUTH-001.md) | Login con Email y Password | P0 | Media | Aprobado | 10 | -| [RF-AUTH-002](./RF-AUTH-002.md) | Generacion y Validacion JWT | P0 | Alta | Aprobado | 9 | -| [RF-AUTH-003](./RF-AUTH-003.md) | Refresh Token y Renovacion | P0 | Media | Aprobado | 10 | -| [RF-AUTH-004](./RF-AUTH-004.md) | Logout y Revocacion | P0 | Baja | Aprobado | 6 | -| [RF-AUTH-005](./RF-AUTH-005.md) | Recuperacion de Password | P1 | Media | Aprobado | 10 | - -**Total Story Points:** 45 - ---- - -## Grafo de Dependencias - -``` -RF-AUTH-001 (Login) - │ - ├──► RF-AUTH-002 (JWT Tokens) - │ │ - │ └──► RF-AUTH-003 (Refresh Token) - │ │ - │ └──► RF-AUTH-004 (Logout) - │ - └──► RF-AUTH-005 (Password Recovery) - │ - └──► RF-AUTH-004 (Logout) [logout-all] -``` - -### Orden de Implementacion Recomendado - -1. **RF-AUTH-001** - Login (base de todo) -2. **RF-AUTH-002** - JWT Tokens (generacion) -3. **RF-AUTH-003** - Refresh Token (renovacion) -4. **RF-AUTH-004** - Logout (cierre de sesion) -5. **RF-AUTH-005** - Password Recovery (recuperacion) - ---- - -## Endpoints del Modulo - -| Metodo | Endpoint | RF | Descripcion | -|--------|----------|-----|-------------| -| POST | `/api/v1/auth/login` | RF-AUTH-001 | Autenticar usuario | -| POST | `/api/v1/auth/refresh` | RF-AUTH-003 | Renovar tokens | -| POST | `/api/v1/auth/logout` | RF-AUTH-004 | Cerrar sesion | -| POST | `/api/v1/auth/logout-all` | RF-AUTH-004 | Cerrar todas las sesiones | -| POST | `/api/v1/auth/password/request-reset` | RF-AUTH-005 | Solicitar recuperacion | -| POST | `/api/v1/auth/password/reset` | RF-AUTH-005 | Cambiar password | -| GET | `/api/v1/auth/password/validate-token/:token` | RF-AUTH-005 | Validar token reset | - ---- - -## Tablas de Base de Datos - -| Tabla | RF | Descripcion | -|-------|-----|-------------| -| `users` | RF-AUTH-001 | Usuarios (existente, agregar columnas) | -| `refresh_tokens` | RF-AUTH-002, RF-AUTH-003 | Tokens de refresh | -| `revoked_tokens` | RF-AUTH-002 | Blacklist de tokens | -| `session_history` | RF-AUTH-001, RF-AUTH-004 | Historial de sesiones | -| `login_attempts` | RF-AUTH-001 | Control de intentos fallidos | -| `password_reset_tokens` | RF-AUTH-005 | Tokens de recuperacion | -| `password_history` | RF-AUTH-005 | Historial de passwords | - ---- - -## Criterios de Aceptacion Consolidados - -### Seguridad - -- [ ] Passwords hasheados con bcrypt (salt rounds = 12) -- [ ] Tokens JWT firmados con RS256 -- [ ] Refresh tokens en httpOnly cookies -- [ ] Access tokens con expiracion corta (15 min) -- [ ] Deteccion de token replay -- [ ] Rate limiting en todos los endpoints -- [ ] No revelar existencia de emails - -### Funcionalidad - -- [ ] Login con email/password funcional -- [ ] Generacion correcta de par de tokens -- [ ] Refresh automatico antes de expiracion -- [ ] Logout individual y global -- [ ] Recuperacion de password via email - -### Auditoria - -- [ ] Todos los logins registrados -- [ ] Todos los logouts registrados -- [ ] Intentos fallidos registrados -- [ ] Cambios de password registrados - ---- - -## Reglas de Negocio Transversales - -| ID | Regla | Aplica a | -|----|-------|----------| -| RN-T001 | Multi-tenancy: tenant_id obligatorio en tokens | Todos | -| RN-T002 | Usuarios pueden tener multiples sesiones | RF-001, RF-003 | -| RN-T003 | Maximo 5 sesiones activas por usuario | RF-001, RF-003 | -| RN-T004 | Bloqueo despues de 5 intentos fallidos | RF-001 | -| RN-T005 | Notificaciones de seguridad via email | RF-004, RF-005 | - ---- - -## Estimacion Total - -| Capa | Story Points | -|------|--------------| -| Database | 9 | -| Backend | 23 | -| Frontend | 13 | -| **Total** | **45** | - ---- - -## Riesgos Identificados - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Vulnerabilidad en manejo de tokens | Media | Alto | Code review, testing seguridad | -| Performance en blacklist | Baja | Medio | Redis con TTL automatico | -| Email delivery failures | Media | Medio | Retry queue, logs | -| Session fixation | Baja | Alto | Regenerar tokens post-login | - ---- - -## Referencias - -### Documentacion Relacionada - -- [DDL-SPEC-core_auth.md](../../02-modelado/database-design/DDL-SPEC-core_auth.md) - Especificacion de base de datos -- [ET-auth-backend.md](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Especificacion tecnica backend -- [TP-auth.md](../../04-test-plans/TP-auth.md) - Plan de pruebas - -### Directivas Aplicables - -- `DIRECTIVA-DOCUMENTACION-PRE-DESARROLLO.md` -- `DIRECTIVA-PATRONES-ODOO.md` -- `ESTANDARES-API-REST-GENERICO.md` - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 5 RFs | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-auth/RF-AUTH-001.md b/docs/03-requerimientos/RF-auth/RF-AUTH-001.md deleted file mode 100644 index 01f27fd..0000000 --- a/docs/03-requerimientos/RF-auth/RF-AUTH-001.md +++ /dev/null @@ -1,234 +0,0 @@ -# RF-AUTH-001: Login con Email y Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-AUTH-001 | -| **Modulo** | MGN-001 | -| **Nombre Modulo** | Auth - Autenticacion | -| **Prioridad** | P0 | -| **Complejidad** | Media | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a los usuarios autenticarse utilizando su correo electronico y contraseña. Al autenticarse exitosamente, el sistema debe generar tokens JWT (access token y refresh token) que permitan al usuario acceder a los recursos protegidos del sistema. - -### Contexto de Negocio - -La autenticacion es la puerta de entrada al sistema ERP. Sin un mecanismo robusto de login, no es posible garantizar la seguridad de los datos ni el aislamiento multi-tenant. Este requerimiento es la base de toda la seguridad del sistema. - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El sistema debe aceptar email y password como credenciales de login -- [x] **CA-002:** El sistema debe validar que el email exista en la base de datos -- [x] **CA-003:** El sistema debe verificar el password usando bcrypt -- [x] **CA-004:** El sistema debe generar un access token JWT con expiracion de 15 minutos -- [x] **CA-005:** El sistema debe generar un refresh token JWT con expiracion de 7 dias -- [x] **CA-006:** El sistema debe registrar el login en el historial de sesiones -- [x] **CA-007:** El sistema debe devolver error 401 si las credenciales son invalidas -- [x] **CA-008:** El sistema debe bloquear la cuenta despues de 5 intentos fallidos - -### Ejemplos de Verificacion - -```gherkin -Scenario: Login exitoso - Given un usuario registrado con email "user@example.com" y password "SecurePass123!" - When el usuario envia credenciales correctas al endpoint /api/v1/auth/login - Then el sistema responde con status 200 - And el body contiene accessToken y refreshToken - And se registra el login en session_history - -Scenario: Login fallido por password incorrecto - Given un usuario registrado con email "user@example.com" - When el usuario envia password incorrecto - Then el sistema responde con status 401 - And el mensaje es "Credenciales invalidas" - And se incrementa el contador de intentos fallidos - -Scenario: Cuenta bloqueada por intentos fallidos - Given un usuario con 5 intentos fallidos de login - When el usuario intenta hacer login nuevamente - Then el sistema responde con status 423 - And el mensaje indica que la cuenta esta bloqueada -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | El email debe ser unico por tenant | Constraint UNIQUE(tenant_id, email) | -| RN-002 | El password debe tener minimo 8 caracteres | Validacion en DTO | -| RN-003 | El password debe incluir mayuscula, minuscula, numero | Regex validation | -| RN-004 | Maximo 5 intentos fallidos antes de bloqueo | Contador en BD | -| RN-005 | El bloqueo dura 30 minutos | Campo locked_until en users | -| RN-006 | Los tokens deben incluir tenant_id en el payload | JWT payload | -| RN-007 | El access token expira en 15 minutos | JWT exp claim | -| RN-008 | El refresh token expira en 7 dias | JWT exp claim | - -### Excepciones - -- Si el usuario tiene 2FA habilitado, el login genera un token temporal y requiere verificacion adicional -- Los superadmins pueden acceder a multiples tenants (tenant_id opcional en su caso) - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Schema | usar | `core_auth` | -| Tabla | usar | `users` - verificar credenciales | -| Tabla | usar | `sessions` - registrar login | -| Tabla | crear | `login_attempts` - control de intentos | -| Columna | agregar | `users.failed_login_attempts` | -| Columna | agregar | `users.locked_until` | -| Indice | crear | `idx_users_email_tenant` | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Service | crear | `AuthService.login()` | -| Service | crear | `TokenService.generateTokens()` | -| Controller | crear | `AuthController.login()` | -| DTO | crear | `LoginDto` | -| DTO | crear | `LoginResponseDto` | -| Endpoints | crear | `POST /api/v1/auth/login` | -| Guard | crear | `ThrottlerGuard` para rate limiting | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Pagina | crear | `LoginPage` | -| Componente | crear | `LoginForm` | -| Store | crear | `authStore` | -| Service | crear | `authService.login()` | -| Hook | crear | `useAuth` | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| - | Ninguna (es el primer RF) | - | - -### Dependencias Relacionadas (No bloqueantes) - -| ID | Requerimiento | Relacion | -|----|---------------|----------| -| RF-AUTH-002 | JWT Tokens | Genera tokens | -| RF-AUTH-003 | Refresh Token | Genera refresh token | -| RF-AUTH-004 | Logout | Usa misma sesion | - ---- - -## Mockups / Wireframes - -### Pantalla de Login - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| +-------------------------+ | -| | INICIAR SESION | | -| +-------------------------+ | -| | -| Email: | -| +-------------------------+ | -| | user@example.com | | -| +-------------------------+ | -| | -| Password: | -| +-------------------------+ | -| | •••••••••••• | | -| +-------------------------+ | -| | -| [ ] Recordarme | -| | -| [ INICIAR SESION ] | -| | -| Olvidaste tu password? | -| | -+------------------------------------------------------------------+ -``` - -### Estados de UI - -| Estado | Comportamiento | -|--------|----------------| -| Loading | Boton deshabilitado, spinner visible | -| Error | Toast rojo con mensaje de error | -| Success | Redirect a dashboard | -| Blocked | Mensaje de cuenta bloqueada con tiempo restante | - ---- - -## Datos de Prueba - -### Escenarios - -| Escenario | Datos Entrada | Resultado Esperado | -|-----------|---------------|-------------------| -| Happy path | email: "test@erp.com", password: "Test123!" | 200, tokens generados | -| Email no existe | email: "noexiste@erp.com" | 401, "Credenciales invalidas" | -| Password incorrecto | password: "wrongpass" | 401, "Credenciales invalidas" | -| Cuenta bloqueada | 5+ intentos fallidos | 423, "Cuenta bloqueada" | -| Email vacio | email: "" | 400, "Email es requerido" | -| Password muy corto | password: "123" | 400, "Password minimo 8 caracteres" | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 2 | Tablas ya existen, solo columnas nuevas | -| Backend | 5 | Service, Controller, DTOs, Guards | -| Frontend | 3 | LoginPage, Form, Store | -| **Total** | **10** | | - ---- - -## Notas Adicionales - -- Usar bcrypt con salt rounds = 12 para hash de passwords -- Los tokens JWT deben firmarse con algoritmo RS256 (asymmetric) -- Implementar rate limiting: max 10 requests/minuto por IP -- Loguear todos los intentos de login (exitosos y fallidos) para auditoria -- Considerar implementar CAPTCHA despues de 3 intentos fallidos - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-auth/RF-AUTH-002.md b/docs/03-requerimientos/RF-auth/RF-AUTH-002.md deleted file mode 100644 index 9abe890..0000000 --- a/docs/03-requerimientos/RF-auth/RF-AUTH-002.md +++ /dev/null @@ -1,264 +0,0 @@ -# RF-AUTH-002: Generacion y Validacion de JWT Tokens - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-AUTH-002 | -| **Modulo** | MGN-001 | -| **Nombre Modulo** | Auth - Autenticacion | -| **Prioridad** | P0 | -| **Complejidad** | Alta | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe implementar un mecanismo de tokens JWT (JSON Web Tokens) para autenticar y autorizar las peticiones de los usuarios. Los tokens deben contener la informacion necesaria para identificar al usuario y su tenant, permitiendo el acceso a los recursos del sistema de forma segura y stateless. - -### Contexto de Negocio - -Los tokens JWT permiten autenticacion stateless, lo cual es esencial para: -- Escalabilidad horizontal del backend -- APIs REST verdaderamente stateless -- Soporte para multiples clientes (web, mobile, integraciones) -- Aislamiento multi-tenant mediante tenant_id en el payload - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El sistema debe generar access tokens con expiracion de 15 minutos -- [x] **CA-002:** El sistema debe generar refresh tokens con expiracion de 7 dias -- [x] **CA-003:** El payload del token debe incluir: userId, tenantId, email, roles -- [x] **CA-004:** Los tokens deben firmarse con algoritmo RS256 (asymmetric keys) -- [x] **CA-005:** El sistema debe validar la firma del token en cada request -- [x] **CA-006:** El sistema debe rechazar tokens expirados con error 401 -- [x] **CA-007:** El sistema debe rechazar tokens con firma invalida con error 401 -- [x] **CA-008:** El sistema debe extraer el usuario del token y adjuntarlo al request - -### Ejemplos de Verificacion - -```gherkin -Scenario: Validacion de token valido - Given un usuario autenticado con un access token valido - When el usuario hace una peticion a un endpoint protegido - Then el sistema valida la firma del token - And extrae el userId y tenantId del payload - And permite el acceso al recurso - -Scenario: Token expirado - Given un usuario con un access token expirado - When el usuario hace una peticion a un endpoint protegido - Then el sistema responde con status 401 - And el mensaje es "Token expirado" - And el header incluye WWW-Authenticate: Bearer error="token_expired" - -Scenario: Token con firma invalida - Given un token modificado o con firma incorrecta - When se intenta acceder a un endpoint protegido - Then el sistema responde con status 401 - And el mensaje es "Token invalido" -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | Access token expira en 15 minutos | JWT exp claim | -| RN-002 | Refresh token expira en 7 dias | JWT exp claim | -| RN-003 | Los tokens usan algoritmo RS256 | Asymmetric signing | -| RN-004 | El payload incluye jti (JWT ID) unico | UUID v4 | -| RN-005 | El issuer (iss) debe ser "erp-core" | JWT iss claim | -| RN-006 | El audience (aud) debe ser "erp-api" | JWT aud claim | -| RN-007 | El tenant_id es obligatorio (excepto superadmin) | Validacion en middleware | - -### Estructura del JWT Payload - -```json -{ - "sub": "user-uuid", // Subject: User ID - "tid": "tenant-uuid", // Tenant ID - "email": "user@example.com", // Email del usuario - "roles": ["admin", "user"], // Roles del usuario - "permissions": ["read", "write"], // Permisos directos - "iat": 1701792000, // Issued At - "exp": 1701792900, // Expiration (15 min) - "iss": "erp-core", // Issuer - "aud": "erp-api", // Audience - "jti": "unique-token-id" // JWT ID -} -``` - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Tabla | crear | `refresh_tokens` - almacenar refresh tokens | -| Tabla | crear | `revoked_tokens` - tokens revocados | -| Columna | - | `refresh_tokens.token_hash` (hash del token) | -| Columna | - | `refresh_tokens.expires_at` | -| Columna | - | `refresh_tokens.device_info` | -| Indice | crear | `idx_refresh_tokens_user` | -| Indice | crear | `idx_revoked_tokens_jti` | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Service | crear | `TokenService` | -| Method | crear | `generateAccessToken()` | -| Method | crear | `generateRefreshToken()` | -| Method | crear | `validateToken()` | -| Method | crear | `decodeToken()` | -| Guard | crear | `JwtAuthGuard` | -| Middleware | crear | `TokenMiddleware` | -| Config | crear | JWT keys (public/private) | -| Interface | crear | `JwtPayload` | -| Interface | crear | `TokenPair` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Service | crear | `tokenService` | -| Method | - | `getAccessToken()` | -| Method | - | `setTokens()` | -| Method | - | `clearTokens()` | -| Method | - | `isTokenExpired()` | -| Interceptor | crear | Axios interceptor para adjuntar token | -| Storage | usar | localStorage/sessionStorage para tokens | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-AUTH-001 | Login | Genera los tokens | - -### Dependencias Relacionadas - -| ID | Requerimiento | Relacion | -|----|---------------|----------| -| RF-AUTH-003 | Refresh Token | Usa refresh token | -| RF-AUTH-004 | Logout | Revoca tokens | - ---- - -## Especificaciones Tecnicas - -### Generacion de Claves RS256 - -```bash -# Generar clave privada -openssl genrsa -out private.key 2048 - -# Extraer clave publica -openssl rsa -in private.key -pubout -out public.key -``` - -### Configuracion de Tokens - -```typescript -// config/jwt.config.ts -export const jwtConfig = { - accessToken: { - algorithm: 'RS256', - expiresIn: '15m', - issuer: 'erp-core', - audience: 'erp-api', - }, - refreshToken: { - algorithm: 'RS256', - expiresIn: '7d', - issuer: 'erp-core', - audience: 'erp-api', - }, -}; -``` - -### Ejemplo de Token Generado - -``` -Header: -{ - "alg": "RS256", - "typ": "JWT" -} - -Payload: -{ - "sub": "550e8400-e29b-41d4-a716-446655440000", - "tid": "tenant-123", - "email": "user@example.com", - "roles": ["admin"], - "iat": 1701792000, - "exp": 1701792900, - "iss": "erp-core", - "aud": "erp-api", - "jti": "abc123" -} -``` - ---- - -## Datos de Prueba - -| Escenario | Token | Resultado | -|-----------|-------|-----------| -| Token valido | JWT firmado correctamente | 200, acceso permitido | -| Token expirado | exp < now | 401, "Token expirado" | -| Firma invalida | Token modificado | 401, "Token invalido" | -| Sin token | Header Authorization vacio | 401, "Token requerido" | -| Formato incorrecto | "Bearer abc123" (no JWT) | 401, "Token malformado" | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 2 | Tablas refresh_tokens, revoked_tokens | -| Backend | 5 | TokenService, Guards, Middleware | -| Frontend | 2 | Token storage e interceptors | -| **Total** | **9** | | - ---- - -## Notas Adicionales - -- Las claves privadas deben almacenarse en variables de entorno o secrets manager -- Considerar rotacion de claves cada 90 dias -- Implementar JWK (JSON Web Key) endpoint para distribuir clave publica -- El refresh token NO debe almacenarse en localStorage (usar httpOnly cookie) -- Implementar token blacklist en Redis para logout inmediato - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-auth/RF-AUTH-003.md b/docs/03-requerimientos/RF-auth/RF-AUTH-003.md deleted file mode 100644 index e734942..0000000 --- a/docs/03-requerimientos/RF-auth/RF-AUTH-003.md +++ /dev/null @@ -1,261 +0,0 @@ -# RF-AUTH-003: Refresh Token y Renovacion de Sesion - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-AUTH-003 | -| **Modulo** | MGN-001 | -| **Nombre Modulo** | Auth - Autenticacion | -| **Prioridad** | P0 | -| **Complejidad** | Media | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir renovar el access token utilizando un refresh token valido, sin requerir que el usuario vuelva a ingresar sus credenciales. Esto permite mantener sesiones de larga duracion de forma segura, mientras los access tokens tienen vida corta. - -### Contexto de Negocio - -Los access tokens tienen vida corta (15 minutos) por seguridad. Sin un mecanismo de refresh, los usuarios tendrian que re-autenticarse constantemente, lo cual afecta negativamente la experiencia de usuario. El refresh token permite mantener la sesion activa hasta 7 dias sin comprometer la seguridad. - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El sistema debe aceptar un refresh token valido y generar nuevos tokens -- [x] **CA-002:** El nuevo access token debe tener los mismos claims que el original -- [x] **CA-003:** El refresh token usado debe invalidarse (rotacion de tokens) -- [x] **CA-004:** Se debe generar un nuevo refresh token con cada renovacion -- [x] **CA-005:** El sistema debe rechazar refresh tokens expirados con error 401 -- [x] **CA-006:** El sistema debe rechazar refresh tokens revocados con error 401 -- [x] **CA-007:** El sistema debe detectar y prevenir reuso de refresh tokens (token replay) -- [x] **CA-008:** El frontend debe renovar automaticamente antes de que expire el access token - -### Ejemplos de Verificacion - -```gherkin -Scenario: Renovacion exitosa de tokens - Given un usuario con refresh token valido - When el usuario envia el refresh token a /api/v1/auth/refresh - Then el sistema invalida el refresh token anterior - And genera un nuevo par de tokens (access + refresh) - And responde con status 200 y los nuevos tokens - -Scenario: Refresh token expirado - Given un usuario con refresh token expirado - When el usuario intenta renovar tokens - Then el sistema responde con status 401 - And el mensaje es "Refresh token expirado" - And el usuario debe hacer login nuevamente - -Scenario: Deteccion de token replay (reuso) - Given un refresh token que ya fue usado para renovar - When alguien intenta usar ese mismo refresh token - Then el sistema detecta el reuso - And invalida TODOS los tokens del usuario (seguridad) - And responde con status 401 - And el mensaje es "Sesion comprometida, por favor inicie sesion" -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | Refresh token valido por 7 dias | JWT exp claim | -| RN-002 | Cada refresh genera nuevo refresh token (rotacion) | Token replacement | -| RN-003 | Refresh token usado se invalida inmediatamente | Marcar como usado en BD | -| RN-004 | Reuso de refresh token invalida toda la familia | Revocar todos tokens del usuario | -| RN-005 | Maximo 5 sesiones activas por usuario | Contador de sesiones | -| RN-006 | El refresh se hace 1 minuto antes de expiracion | Frontend timer | - -### Token Family (Familia de Tokens) - -Cada refresh token pertenece a una "familia" que se origina en un login. Si se detecta reuso de un token de esa familia, toda la familia se invalida. - -``` -Login -> RT1 -> RT2 -> RT3 (familia activa) - ↳ RT2 reusado? -> Invalida RT1, RT2, RT3 -``` - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Tabla | modificar | `refresh_tokens` - agregar campos | -| Columna | agregar | `family_id` UUID - familia del token | -| Columna | agregar | `is_used` BOOLEAN - si ya fue usado | -| Columna | agregar | `used_at` TIMESTAMPTZ - cuando se uso | -| Columna | agregar | `replaced_by` UUID - token que lo reemplazo | -| Indice | crear | `idx_refresh_tokens_family` | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Controller | crear | `AuthController.refresh()` | -| Method | crear | `TokenService.refreshTokens()` | -| Method | crear | `TokenService.detectTokenReuse()` | -| Method | crear | `TokenService.revokeTokenFamily()` | -| DTO | crear | `RefreshTokenDto` | -| DTO | crear | `TokenResponseDto` | -| Endpoint | crear | `POST /api/v1/auth/refresh` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Service | modificar | `tokenService.refreshTokens()` | -| Interceptor | crear | Auto-refresh interceptor | -| Timer | crear | Refresh timer (1 min antes de exp) | -| Handler | crear | Token expiration handler | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-AUTH-001 | Login | Genera tokens iniciales | -| RF-AUTH-002 | JWT Tokens | Estructura de tokens | - -### Dependencias Relacionadas - -| ID | Requerimiento | Relacion | -|----|---------------|----------| -| RF-AUTH-004 | Logout | Revoca refresh tokens | - ---- - -## Especificaciones Tecnicas - -### Flujo de Refresh - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. Frontend detecta que access token expira pronto │ -│ (1 minuto antes de exp) │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 2. Frontend envia refresh token a POST /api/v1/auth/refresh │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 3. Backend valida refresh token │ -│ - Verifica firma │ -│ - Verifica expiracion │ -│ - Verifica que no este usado (is_used = false) │ -│ - Verifica que no este revocado │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 4. Si es valido: │ -│ - Marca token actual como usado (is_used = true) │ -│ - Genera nuevo access token │ -│ - Genera nuevo refresh token (misma family_id) │ -│ - Actualiza replaced_by del token anterior │ -│ - Retorna nuevos tokens │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 5. Frontend almacena nuevos tokens │ -│ - Actualiza access token en memoria/storage │ -│ - Actualiza refresh token en httpOnly cookie │ -│ - Reinicia timer de refresh │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Deteccion de Token Replay - -```typescript -async refreshTokens(refreshToken: string): Promise { - const decoded = this.decodeToken(refreshToken); - const storedToken = await this.refreshTokenRepo.findOne({ - where: { jti: decoded.jti } - }); - - // Detectar reuso - if (storedToken.isUsed) { - // ALERTA: Token replay detectado - await this.revokeTokenFamily(storedToken.familyId); - throw new UnauthorizedException('Sesion comprometida'); - } - - // Marcar como usado - storedToken.isUsed = true; - storedToken.usedAt = new Date(); - - // Generar nuevos tokens - const newTokens = await this.generateTokenPair(decoded.sub, decoded.tid); - - // Vincular tokens - storedToken.replacedBy = newTokens.refreshTokenId; - await this.refreshTokenRepo.save(storedToken); - - return newTokens; -} -``` - ---- - -## Datos de Prueba - -| Escenario | Entrada | Resultado | -|-----------|---------|-----------| -| Refresh exitoso | Refresh token valido | 200, nuevos tokens | -| Token expirado | RT con exp < now | 401, "Token expirado" | -| Token ya usado | RT con is_used = true | 401, "Sesion comprometida" | -| Token revocado | RT en revoked_tokens | 401, "Token revocado" | -| Sin refresh token | Body vacio | 400, "Refresh token requerido" | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 2 | Columnas adicionales en refresh_tokens | -| Backend | 5 | Logica de rotacion y deteccion reuso | -| Frontend | 3 | Auto-refresh interceptor y timer | -| **Total** | **10** | | - ---- - -## Notas Adicionales - -- El refresh token debe enviarse en httpOnly cookie, no en body (previene XSS) -- Considerar sliding window: extender expiracion si hay actividad -- Implementar rate limiting en endpoint de refresh (max 1 req/segundo) -- Loguear todos los refreshes para auditoria -- En caso de breach, proporcionar endpoint para revocar todas las sesiones - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-auth/RF-AUTH-004.md b/docs/03-requerimientos/RF-auth/RF-AUTH-004.md deleted file mode 100644 index 3371fbf..0000000 --- a/docs/03-requerimientos/RF-auth/RF-AUTH-004.md +++ /dev/null @@ -1,288 +0,0 @@ -# RF-AUTH-004: Logout y Revocacion de Sesion - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-AUTH-004 | -| **Modulo** | MGN-001 | -| **Nombre Modulo** | Auth - Autenticacion | -| **Prioridad** | P0 | -| **Complejidad** | Baja | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a los usuarios cerrar su sesion de forma segura, revocando todos los tokens asociados (access token y refresh token). Esto garantiza que los tokens no puedan ser reutilizados despues del logout, incluso si no han expirado. - -### Contexto de Negocio - -El logout seguro es esencial para: -- Proteger cuentas en dispositivos compartidos -- Cumplir con politicas de seguridad corporativas -- Permitir al usuario revocar acceso si sospecha compromiso -- Terminar sesiones en dispositivos perdidos o robados - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El sistema debe aceptar el refresh token para identificar la sesion a cerrar -- [x] **CA-002:** El sistema debe invalidar el refresh token en la base de datos -- [x] **CA-003:** El sistema debe agregar el access token actual a la blacklist -- [x] **CA-004:** El sistema debe eliminar la cookie httpOnly del refresh token -- [x] **CA-005:** El sistema debe responder con 200 OK en logout exitoso -- [x] **CA-006:** El sistema debe registrar el logout en el historial de sesiones -- [x] **CA-007:** El sistema debe permitir logout de todas las sesiones (logout global) -- [x] **CA-008:** El frontend debe limpiar tokens de memoria/storage - -### Ejemplos de Verificacion - -```gherkin -Scenario: Logout exitoso - Given un usuario autenticado con sesion activa - When el usuario hace POST /api/v1/auth/logout - Then el sistema revoca el refresh token actual - And agrega el access token a la blacklist - And elimina la cookie del refresh token - And responde con status 200 - And registra el evento en session_history - -Scenario: Logout de todas las sesiones - Given un usuario con multiples sesiones activas en diferentes dispositivos - When el usuario hace POST /api/v1/auth/logout-all - Then el sistema revoca TODOS los refresh tokens del usuario - And invalida toda la familia de tokens - And responde con status 200 - And el usuario es forzado a re-autenticarse en todos los dispositivos - -Scenario: Logout con token ya expirado - Given un usuario con access token expirado pero refresh token valido - When el usuario intenta hacer logout - Then el sistema permite el logout usando solo el refresh token - And responde con status 200 -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | El logout revoca la sesion actual unicamente | Por defecto solo sesion actual | -| RN-002 | Logout-all revoca todas las sesiones del usuario | Parametro all=true | -| RN-003 | Los tokens revocados se almacenan hasta su expiracion natural | Cleanup job posterior | -| RN-004 | El access token se blacklistea en Redis/memoria | TTL = tiempo restante de exp | -| RN-005 | El logout debe funcionar aunque el access token este expirado | Usar refresh token | -| RN-006 | El evento de logout se registra para auditoria | session_history.action = 'logout' | - -### Blacklist de Tokens - -Para invalidacion inmediata de access tokens (que son stateless), se usa una blacklist: - -``` -Token activo + logout → jti agregado a blacklist -Validacion de token → verificar si jti esta en blacklist -Blacklist TTL → igual al tiempo restante del token -``` - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Tabla | usar | `refresh_tokens` - marcar como revocado | -| Tabla | usar | `session_history` - registrar logout | -| Columna | agregar | `refresh_tokens.revoked_at` TIMESTAMPTZ | -| Columna | agregar | `refresh_tokens.revoked_reason` VARCHAR(50) | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Controller | crear | `AuthController.logout()` | -| Controller | crear | `AuthController.logoutAll()` | -| Method | crear | `TokenService.revokeRefreshToken()` | -| Method | crear | `TokenService.revokeAllUserTokens()` | -| Method | crear | `TokenService.blacklistAccessToken()` | -| Service | crear | `BlacklistService` (Redis) | -| Endpoint | crear | `POST /api/v1/auth/logout` | -| Endpoint | crear | `POST /api/v1/auth/logout-all` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Service | modificar | `authService.logout()` | -| Store | modificar | `authStore.clearSession()` | -| Method | crear | `tokenService.clearAllTokens()` | -| Interceptor | modificar | Redirect a login en 401 post-logout | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-AUTH-001 | Login | Crea la sesion a cerrar | -| RF-AUTH-002 | JWT Tokens | Tokens a revocar | -| RF-AUTH-003 | Refresh Token | Token a invalidar | - -### Dependencias Relacionadas - -| ID | Requerimiento | Relacion | -|----|---------------|----------| -| RF-AUTH-005 | Password Recovery | Puede forzar logout-all | - ---- - -## Especificaciones Tecnicas - -### Flujo de Logout - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. Frontend llama POST /api/v1/auth/logout │ -│ - Envia refresh token en cookie httpOnly │ -│ - Envia access token en header Authorization │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 2. Backend extrae tokens │ -│ - Decodifica refresh token para obtener jti │ -│ - Decodifica access token para obtener jti │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 3. Revoca refresh token en BD │ -│ - UPDATE refresh_tokens SET │ -│ revoked_at = NOW(), │ -│ revoked_reason = 'user_logout' │ -│ WHERE jti = :refresh_jti │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 4. Blacklistea access token en Redis │ -│ - SET blacklist:{access_jti} = 1 │ -│ - EXPIRE blacklist:{access_jti} {remaining_ttl} │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 5. Elimina cookie de refresh token │ -│ - Set-Cookie: refresh_token=; Max-Age=0; HttpOnly │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 6. Registra evento en session_history │ -│ - INSERT INTO session_history (user_id, action, ...) │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 7. Responde al frontend │ -│ - Status 200 OK │ -│ - Body: { message: "Sesion cerrada exitosamente" } │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Blacklist Service (Redis) - -```typescript -@Injectable() -export class BlacklistService { - constructor(private redis: RedisService) {} - - async blacklistToken(jti: string, expiresIn: number): Promise { - const key = `blacklist:${jti}`; - await this.redis.set(key, '1', 'EX', expiresIn); - } - - async isBlacklisted(jti: string): Promise { - const key = `blacklist:${jti}`; - const result = await this.redis.get(key); - return result !== null; - } -} -``` - -### Logout All (Logout Global) - -```typescript -async logoutAll(userId: string): Promise { - // Revocar todos los refresh tokens del usuario - await this.refreshTokenRepo.update( - { userId, revokedAt: IsNull() }, - { revokedAt: new Date(), revokedReason: 'logout_all' } - ); - - // Blacklistear todos los access tokens activos - // (requiere tracking de tokens activos o usar family_id) - await this.blacklistUserTokens(userId); - - // Registrar evento - await this.sessionHistoryService.record({ - userId, - action: 'logout_all', - metadata: { reason: 'user_requested' } - }); -} -``` - ---- - -## Datos de Prueba - -| Escenario | Entrada | Resultado | -|-----------|---------|-----------| -| Logout exitoso | Token valido | 200, sesion cerrada | -| Logout sin token | Sin Authorization | 401, "Token requerido" | -| Logout token expirado | Access expirado, refresh valido | 200, permite logout | -| Logout-all | all=true | 200, todas sesiones cerradas | -| Logout token ya revocado | Refresh ya revocado | 200, idempotente | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 1 | Columnas adicionales | -| Backend | 3 | Controller, Services, Redis | -| Frontend | 2 | Limpieza de estado | -| **Total** | **6** | | - ---- - -## Notas Adicionales - -- Implementar logout como operacion idempotente (no falla si ya esta logged out) -- Considerar endpoint para logout de sesion especifica por device_id -- El blacklist en Redis debe tener alta disponibilidad -- Cleanup job para eliminar tokens revocados expirados de BD -- Notificar al usuario via email si se hace logout-all (seguridad) - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-auth/RF-AUTH-005.md b/docs/03-requerimientos/RF-auth/RF-AUTH-005.md deleted file mode 100644 index 977ec6b..0000000 --- a/docs/03-requerimientos/RF-auth/RF-AUTH-005.md +++ /dev/null @@ -1,345 +0,0 @@ -# RF-AUTH-005: Recuperacion de Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-AUTH-005 | -| **Modulo** | MGN-001 | -| **Nombre Modulo** | Auth - Autenticacion | -| **Prioridad** | P1 | -| **Complejidad** | Media | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a los usuarios recuperar el acceso a su cuenta cuando olvidan su contraseña. El proceso incluye solicitar un enlace de recuperacion por email, validar el token de recuperacion, y establecer una nueva contraseña de forma segura. - -### Contexto de Negocio - -La recuperacion de password es un proceso critico que debe balancear: -- Usabilidad: El usuario debe poder recuperar acceso facilmente -- Seguridad: El proceso no debe permitir acceso no autorizado -- Cumplimiento: Debe registrarse para auditoria y prevencion de abuso - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El sistema debe generar un token unico de recuperacion con expiracion de 1 hora -- [x] **CA-002:** El sistema debe enviar email con enlace de recuperacion -- [x] **CA-003:** El sistema debe validar el token antes de permitir cambio de password -- [x] **CA-004:** El sistema debe invalidar el token despues de un uso exitoso -- [x] **CA-005:** El sistema debe invalidar el token despues de 3 intentos fallidos -- [x] **CA-006:** El sistema debe aplicar las mismas reglas de complejidad al nuevo password -- [x] **CA-007:** El sistema debe forzar logout de todas las sesiones al cambiar password -- [x] **CA-008:** El sistema debe notificar al usuario via email que su password fue cambiado -- [x] **CA-009:** El sistema NO debe revelar si el email existe o no (seguridad) - -### Ejemplos de Verificacion - -```gherkin -Scenario: Solicitud de recuperacion exitosa - Given un usuario registrado con email "user@example.com" - When el usuario solicita recuperacion de password - Then el sistema genera un token de recuperacion - And envia email con enlace de recuperacion - And responde con mensaje generico "Si el email existe, recibiras instrucciones" - And el token expira en 1 hora - -Scenario: Cambio de password exitoso - Given un usuario con token de recuperacion valido - When el usuario envia nuevo password cumpliendo requisitos - Then el sistema actualiza el password hasheado - And invalida el token de recuperacion - And cierra todas las sesiones activas del usuario - And envia email confirmando el cambio - And responde con status 200 - -Scenario: Token de recuperacion expirado - Given un token de recuperacion emitido hace mas de 1 hora - When el usuario intenta usarlo para cambiar password - Then el sistema responde con status 400 - And el mensaje es "Token de recuperacion expirado" - -Scenario: Email no registrado (seguridad) - Given un email que NO existe en el sistema - When alguien solicita recuperacion para ese email - Then el sistema responde con el mismo mensaje generico - And NO envia ningun email - And NO revela que el email no existe -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | El token de recuperacion expira en 1 hora | Campo expires_at | -| RN-002 | Solo un token activo por usuario | Invalida tokens anteriores | -| RN-003 | Maximo 3 solicitudes por hora por email | Rate limiting | -| RN-004 | El nuevo password no puede ser igual al anterior | Comparar hashes | -| RN-005 | El nuevo password debe cumplir politica de complejidad | Min 8 chars, mayus, minus, numero | -| RN-006 | Cambio de password fuerza logout-all | Seguridad | -| RN-007 | Respuesta generica para solicitud (no revelar existencia) | Mensaje fijo | -| RN-008 | Token de uso unico | Invalida inmediatamente despues de uso | - -### Politica de Complejidad de Password - -``` -- Minimo 8 caracteres -- Al menos 1 letra mayuscula -- Al menos 1 letra minuscula -- Al menos 1 numero -- Al menos 1 caracter especial (!@#$%^&*) -- No puede contener el email del usuario -- No puede ser igual a los ultimos 5 passwords -``` - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Tabla | crear | `password_reset_tokens` | -| Columna | - | `id` UUID PK | -| Columna | - | `user_id` UUID FK → users | -| Columna | - | `token_hash` VARCHAR(255) | -| Columna | - | `expires_at` TIMESTAMPTZ | -| Columna | - | `used_at` TIMESTAMPTZ NULL | -| Columna | - | `attempts` INTEGER DEFAULT 0 | -| Columna | - | `created_at` TIMESTAMPTZ | -| Tabla | crear | `password_history` - historial de passwords | -| Indice | crear | `idx_password_reset_tokens_user` | -| Indice | crear | `idx_password_reset_tokens_expires` | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Controller | crear | `AuthController.requestPasswordReset()` | -| Controller | crear | `AuthController.resetPassword()` | -| Method | crear | `PasswordService.generateResetToken()` | -| Method | crear | `PasswordService.validateResetToken()` | -| Method | crear | `PasswordService.resetPassword()` | -| Method | crear | `PasswordService.validatePasswordPolicy()` | -| DTO | crear | `RequestPasswordResetDto` | -| DTO | crear | `ResetPasswordDto` | -| Service | usar | `EmailService.sendPasswordResetEmail()` | -| Endpoint | crear | `POST /api/v1/auth/password/request-reset` | -| Endpoint | crear | `POST /api/v1/auth/password/reset` | -| Endpoint | crear | `GET /api/v1/auth/password/validate-token/:token` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Pagina | crear | `ForgotPasswordPage` | -| Pagina | crear | `ResetPasswordPage` | -| Componente | crear | `ForgotPasswordForm` | -| Componente | crear | `ResetPasswordForm` | -| Componente | crear | `PasswordStrengthIndicator` | -| Service | crear | `passwordService.requestReset()` | -| Service | crear | `passwordService.resetPassword()` | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-AUTH-001 | Login | Estructura de usuarios | -| RF-AUTH-004 | Logout | Logout-all despues de cambio | - -### Dependencias Externas - -| Servicio | Descripcion | -|----------|-------------| -| Email Service | Envio de emails transaccionales | -| Template Engine | Templates de email | - ---- - -## Especificaciones Tecnicas - -### Flujo de Recuperacion - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ FASE 1: SOLICITUD DE RECUPERACION │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. Usuario ingresa email en formulario │ -│ 2. Frontend POST /api/v1/auth/password/request-reset │ -│ 3. Backend busca usuario por email │ -│ - Si existe: genera token, envia email │ -│ - Si no existe: no hace nada (seguridad) │ -│ 4. Backend responde con mensaje generico │ -│ "Si el email esta registrado, recibiras instrucciones" │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ FASE 2: EMAIL DE RECUPERACION │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. Usuario recibe email con enlace │ -│ https://app.erp.com/reset-password?token={token} │ -│ 2. El enlace incluye token de uso unico (NO el hash) │ -│ 3. El token tiene validez de 1 hora │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ FASE 3: VALIDACION DE TOKEN │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. Usuario hace clic en enlace │ -│ 2. Frontend GET /api/v1/auth/password/validate-token/:token │ -│ 3. Backend valida: │ -│ - Token existe │ -│ - Token no expirado │ -│ - Token no usado │ -│ - Intentos < 3 │ -│ 4. Si valido: muestra formulario de nuevo password │ -│ Si invalido: muestra error apropiado │ -└─────────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ FASE 4: CAMBIO DE PASSWORD │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. Usuario ingresa nuevo password (2 veces) │ -│ 2. Frontend POST /api/v1/auth/password/reset │ -│ Body: { token, newPassword } │ -│ 3. Backend: │ -│ a. Valida token nuevamente │ -│ b. Valida politica de password │ -│ c. Verifica no sea igual a anteriores │ -│ d. Hashea nuevo password │ -│ e. Actualiza users.password_hash │ -│ f. Marca token como usado │ -│ g. Guarda en password_history │ -│ h. Ejecuta logout-all │ -│ i. Envia email de confirmacion │ -│ 4. Responde 200 OK │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Generacion de Token Seguro - -```typescript -async generateResetToken(email: string): Promise { - const user = await this.userRepo.findByEmail(email); - - // IMPORTANTE: No revelar si el usuario existe - if (!user) { - // Log para auditoria pero no revelar al cliente - this.logger.warn(`Password reset requested for non-existent email: ${email}`); - return; // Respuesta identica a caso exitoso - } - - // Invalida tokens anteriores - await this.passwordResetRepo.invalidateUserTokens(user.id); - - // Genera token seguro (32 bytes = 256 bits) - const token = crypto.randomBytes(32).toString('hex'); - const tokenHash = await bcrypt.hash(token, 10); - - // Guarda en BD - await this.passwordResetRepo.create({ - userId: user.id, - tokenHash, - expiresAt: addHours(new Date(), 1), - attempts: 0, - }); - - // Envia email (async, no bloquea respuesta) - await this.emailService.sendPasswordResetEmail(user.email, token); -} -``` - -### Template de Email - -```html -

Recuperacion de Contraseña

-

Hola {{userName}},

-

Recibimos una solicitud para restablecer tu contraseña.

-

Haz clic en el siguiente enlace para crear una nueva contraseña:

-Restablecer Contraseña -

Este enlace expira en 1 hora.

-

Si no solicitaste este cambio, ignora este email.

-

Por seguridad, nunca compartas este enlace.

-``` - ---- - -## Datos de Prueba - -| Escenario | Entrada | Resultado | -|-----------|---------|-----------| -| Solicitud email existente | email: "test@erp.com" | 200, email enviado | -| Solicitud email no existe | email: "noexiste@erp.com" | 200, mensaje generico (no revela) | -| Token valido | Token < 1 hora | 200, permite cambio | -| Token expirado | Token > 1 hora | 400, "Token expirado" | -| Token usado | Token ya utilizado | 400, "Token ya utilizado" | -| Password debil | "123456" | 400, "Password no cumple requisitos" | -| Password igual anterior | Mismo que actual | 400, "No puede ser igual al anterior" | -| Demasiados intentos | 3+ intentos fallidos | 400, "Token invalidado" | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 2 | Tablas password_reset_tokens, password_history | -| Backend | 5 | Services, validaciones, email | -| Frontend | 3 | Formularios, validacion, UX | -| **Total** | **10** | | - ---- - -## Notas Adicionales - -- Usar crypto.randomBytes para generacion de tokens (no UUID) -- Almacenar solo el HASH del token, no el token plano -- Implementar rate limiting estricto para prevenir enumeracion de emails -- Los enlaces de reset deben ser HTTPS obligatoriamente -- Considerar CAPTCHA para solicitudes de recuperacion -- Implementar honeypot para detectar bots -- El email de confirmacion de cambio debe incluir IP y timestamp - ---- - -## Consideraciones de Seguridad - -| Amenaza | Mitigacion | -|---------|------------| -| Enumeracion de emails | Respuesta identica para email existe/no existe | -| Fuerza bruta en token | Token de 256 bits, max 3 intentos | -| Intercepcion de email | HTTPS, token de uso unico | -| Session fixation | Logout-all despues de cambio | -| Password spraying | Rate limiting, CAPTCHA | - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-catalogs/INDICE-RF-CATALOG.md b/docs/03-requerimientos/RF-catalogs/INDICE-RF-CATALOG.md deleted file mode 100644 index 419f563..0000000 --- a/docs/03-requerimientos/RF-catalogs/INDICE-RF-CATALOG.md +++ /dev/null @@ -1,184 +0,0 @@ -# Indice de Requerimientos: MGN-005 Catalogs - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-005 | -| **Nombre** | Catalogs - Catalogos Maestros | -| **Prioridad** | P0 (Core) | -| **Version** | 1.0 | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion General - -El modulo de Catalogos proporciona datos maestros compartidos y especificos de tenant para el funcionamiento del ERP. Se divide en: - -1. **Catalogos Globales** - Datos compartidos por todos los tenants (paises, monedas) -2. **Catalogos por Tenant** - Datos especificos de cada tenant (contacts, categorias, UoM) - -**Referencia Odoo:** -- `res.partner` - Contacts (clientes, proveedores, empleados) -- `res.country` / `res.country.state` - Paises y estados -- `res.currency` / `res.currency.rate` - Monedas y tasas -- `uom.category` / `uom.uom` - Unidades de medida -- `res.partner.category` - Tags/categorias de contactos - ---- - -## Resumen de Requerimientos - -| ID | Titulo | Prioridad | Estado | -|----|--------|-----------|--------| -| RF-CATALOG-001 | Gestion de Contactos | Alta | Draft | -| RF-CATALOG-002 | Paises y Estados (Global) | Alta | Draft | -| RF-CATALOG-003 | Monedas y Tasas de Cambio (Global) | Alta | Draft | -| RF-CATALOG-004 | Unidades de Medida | Media | Draft | -| RF-CATALOG-005 | Categorias y Tags | Media | Draft | - ---- - -## Arquitectura de Catalogos - -### Catalogos Globales (Sin tenant_id) - -Estos catalogos son compartidos por todos los tenants y no tienen `tenant_id`: - -| Catalogo | Tabla | Descripcion | -|----------|-------|-------------| -| Paises | `core_catalogs.countries` | ISO 3166-1 (250+ paises) | -| Estados | `core_catalogs.states` | Subdivisiones por pais | -| Monedas | `core_catalogs.currencies` | ISO 4217 (180+ monedas) | -| Moneda base | USD | Todas las tasas se referencian a USD | - -**Rationale:** Estos datos son estandares internacionales que no deben variar por tenant. - -### Catalogos por Tenant (Con tenant_id) - -Estos catalogos tienen `tenant_id` y RLS aplicado: - -| Catalogo | Tabla | Descripcion | -|----------|-------|-------------| -| Contactos | `core_catalogs.contacts` | Clientes, proveedores, etc. | -| Categorias UoM | `core_catalogs.uom_categories` | Categorias de unidades | -| Unidades | `core_catalogs.uom` | Unidades de medida | -| Contact Tags | `core_catalogs.contact_tags` | Etiquetas para contactos | -| Tasas de cambio | `core_catalogs.currency_rates` | Tasas por tenant | - ---- - -## Modelo de Contactos - -### Tipos de Contacto (Ref: Odoo res.partner) - -``` -CONTACT_TYPE: - - company # Empresa/Organizacion - - individual # Persona fisica - - contact # Contacto de empresa (child) -``` - -### Roles de Contacto - -Un contacto puede tener multiples roles: - -``` -CONTACT_ROLES: - - customer # Cliente (puede comprar) - - vendor # Proveedor (puede vender) - - employee # Empleado (puede ser usuario) - - other # Otro tipo -``` - -### Campos Principales - -``` -Datos basicos: - - name, display_name - - contact_type (company/individual/contact) - - parent_id (para contactos de empresa) - - roles[] (customer, vendor, employee) - -Identificacion: - - ref (codigo interno) - - vat (RFC/NIT/RUT) - - company_registry - -Comunicacion: - - email, phone, mobile - - website - -Direccion: - - street, street2, city, zip - - state_id, country_id - -Contabilidad: - - currency_id (moneda preferida) - - payment_terms (condiciones de pago) - - credit_limit - -Relaciones: - - user_id (si es un usuario del sistema) - - commercial_partner_id (empresa matriz) -``` - ---- - -## Dependencias - -### Requiere - -| Modulo | Razon | -|--------|-------| -| MGN-001 Auth | Campos created_by, updated_by | -| MGN-004 Tenants | tenant_id para catalogos por tenant | - -### Requerido por - -| Modulo | Razon | -|--------|-------| -| MGN-010 Financial | Cuentas por pais, moneda | -| MGN-011 Inventory | Productos, UoM | -| MGN-012 Purchasing | Proveedores | -| MGN-013 CRM | Clientes, contactos | - ---- - -## Metricas de Exito - -| Metrica | Objetivo | -|---------|----------| -| Contactos por tenant | Sin limite | -| Busqueda de contactos | < 100ms | -| Tasas de cambio por moneda | Historico completo | -| UoM conversiones | Precision 6 decimales | - ---- - -## Notas de Implementacion - -### Diferencias con Odoo - -| Aspecto | Odoo | ERP Suite | -|---------|------|-----------| -| Paises/Monedas | Por company (company_id) | Global (sin tenant_id) | -| Partner/User | Herencia delegada | Tablas separadas con FK | -| UoM | Global | Por tenant | -| Currency rates | Por company | Por tenant | - -### Seed de Datos - -Los catalogos globales (paises, monedas) se cargan desde archivos ISO: -- ISO 3166-1 para paises -- ISO 4217 para monedas -- Datos iniciales proporcionados en migracion - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial basada en Odoo | diff --git a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-001.md b/docs/03-requerimientos/RF-catalogs/RF-CATALOG-001.md deleted file mode 100644 index cad44cf..0000000 --- a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-001.md +++ /dev/null @@ -1,294 +0,0 @@ -# RF-CATALOG-001: Gestion de Contactos - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-CATALOG-001 | -| **Modulo** | MGN-005 Catalogs | -| **Titulo** | Gestion de Contactos | -| **Prioridad** | Alta | -| **Estado** | Draft | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir gestionar contactos (empresas, personas, direcciones) que representan clientes, proveedores, empleados y otros terceros relacionados con el tenant. - -**Referencia Odoo:** `res.partner` (odoo/addons/base/models/res_partner.py) - ---- - -## Requisitos Funcionales - -### RF-CATALOG-001.1: Tipos de Contacto - -El sistema debe soportar tres tipos de contacto: - -| Tipo | Descripcion | Puede tener hijos | -|------|-------------|-------------------| -| `company` | Empresa/Organizacion | Si | -| `individual` | Persona fisica independiente | No | -| `contact` | Contacto de una empresa (empleado) | No | - -**Reglas:** -- Un contacto tipo `contact` DEBE tener `parent_id` (empresa padre) -- Un contacto tipo `company` puede tener contactos hijos -- Un contacto tipo `individual` NO puede tener hijos - -### RF-CATALOG-001.2: Roles de Contacto - -Un contacto puede tener uno o mas roles: - -```typescript -enum ContactRole { - CUSTOMER = 'customer', // Puede comprar - VENDOR = 'vendor', // Puede vender - EMPLOYEE = 'employee', // Empleado interno - OTHER = 'other' // Otro tipo -} -``` - -**Reglas:** -- Un contacto puede ser cliente Y proveedor simultaneamente -- El rol `employee` permite vincular con tabla `users` -- Los roles determinan visibilidad en diferentes modulos - -### RF-CATALOG-001.3: Datos de Identificacion - -Campos obligatorios y opcionales de identificacion: - -| Campo | Obligatorio | Descripcion | -|-------|-------------|-------------| -| name | Si | Nombre completo | -| display_name | Auto | Computed: incluye parent name | -| ref | No | Codigo interno del tenant | -| vat | Condicional | RFC/NIT/RUT (obligatorio si es facturador) | -| company_registry | No | Registro mercantil | - -**Validaciones:** -- VAT debe validarse segun formato del pais -- REF debe ser unico dentro del tenant - -### RF-CATALOG-001.4: Datos de Contacto - -| Campo | Tipo | Validacion | -|-------|------|------------| -| email | string | Formato email valido | -| phone | string | Formato segun pais | -| mobile | string | Formato segun pais | -| website | string | URL valida | - -### RF-CATALOG-001.5: Direcciones - -El sistema debe soportar direccion estructurada: - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| street | string | Calle principal | -| street2 | string | Linea adicional | -| city | string | Ciudad | -| zip | string | Codigo postal | -| state_id | FK | Estado/Provincia (global) | -| country_id | FK | Pais (global) | - -**Referencia:** El formato de direccion varia por pais (ver `res.country.address_format`) - -### RF-CATALOG-001.6: Contactos Hijos - -Para empresas, se pueden agregar contactos relacionados: - -```typescript -interface ContactChild { - parent_id: UUID; // Empresa padre - contact_type: 'contact'; // Siempre 'contact' - function: string; // Cargo/Puesto - name: string; // Nombre del contacto - email: string; // Email directo - phone: string; // Telefono directo -} -``` - -**Reglas:** -- Un contacto hijo hereda direccion del padre si no tiene propia -- Al eliminar empresa, contactos hijos se eliminan en cascada - -### RF-CATALOG-001.7: Vinculacion con Usuarios - -Un contacto puede vincularse con un usuario del sistema: - -```typescript -interface ContactUserLink { - contact_id: UUID; - user_id: UUID; // FK a core_users.users -} -``` - -**Reglas:** -- Un usuario puede tener UN contacto asociado -- Un contacto puede tener UN usuario asociado -- Esta relacion se usa para empleados que son usuarios - -**Diferencia con Odoo:** En Odoo, `res.users` hereda de `res.partner` mediante `_inherits`. Nosotros usamos FK explicita para mayor claridad. - -### RF-CATALOG-001.8: Campos Comerciales - -Para clientes/proveedores: - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| currency_id | FK | Moneda preferida | -| payment_term_days | int | Dias de credito | -| credit_limit | decimal | Limite de credito | -| bank_accounts | JSONB | Cuentas bancarias | - -### RF-CATALOG-001.9: Tags/Categorias - -Los contactos pueden tener multiples tags: - -```sql --- Tabla de tags -core_catalogs.contact_tags ( - id, tenant_id, name, color, parent_id -) - --- Relacion M:N -core_catalogs.contact_tag_rel ( - contact_id, tag_id -) -``` - ---- - -## Operaciones CRUD - -### Crear Contacto - -```typescript -POST /api/v1/contacts -{ - "name": "Empresa Demo S.A.", - "contactType": "company", - "roles": ["customer", "vendor"], - "vat": "RFC123456789", - "email": "contacto@demo.com", - "phone": "+52 55 1234 5678", - "address": { - "street": "Av. Principal 123", - "city": "Ciudad de Mexico", - "stateId": "uuid-estado", - "countryId": "uuid-mexico", - "zip": "06600" - }, - "tags": ["uuid-tag-1", "uuid-tag-2"] -} -``` - -### Listar Contactos - -```typescript -GET /api/v1/contacts?role=customer&type=company&search=demo&page=1&limit=20 - -Response: -{ - "data": [...], - "meta": { "total": 100, "page": 1, "limit": 20 } -} -``` - -### Busqueda Rapida - -```typescript -GET /api/v1/contacts/search?q=demo&limit=10 - -// Busca en: name, email, vat, ref, phone -``` - -### Obtener Contacto con Hijos - -```typescript -GET /api/v1/contacts/:id?include=children,tags - -Response: -{ - "id": "uuid", - "name": "Empresa Demo", - "children": [...], - "tags": [...] -} -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Severidad | -|----|-------|-----------| -| BR-001 | VAT unico por tenant (si existe) | Error | -| BR-002 | REF unico por tenant (si existe) | Error | -| BR-003 | Email formato valido | Error | -| BR-004 | Contact type 'contact' requiere parent_id | Error | -| BR-005 | No eliminar contacto con transacciones | Warning | -| BR-006 | credit_limit >= 0 | Error | - ---- - -## Casos de Prueba - -| ID | Escenario | Resultado Esperado | -|----|-----------|-------------------| -| TC-001 | Crear empresa con datos completos | Contacto creado exitosamente | -| TC-002 | Crear contacto tipo 'contact' sin parent | Error: parent_id requerido | -| TC-003 | Crear contacto con VAT duplicado | Error: VAT ya existe | -| TC-004 | Buscar por nombre parcial | Lista filtrada correctamente | -| TC-005 | Agregar contacto hijo a empresa | Relacion creada | -| TC-006 | Eliminar empresa con hijos | Cascada aplicada | -| TC-007 | Vincular contacto con usuario | Relacion 1:1 creada | - ---- - -## Criterios de Aceptacion - -- [ ] CRUD completo de contactos -- [ ] Soporte para 3 tipos de contacto -- [ ] Relacion padre-hijo funcional -- [ ] Tags/categorias asignables -- [ ] Busqueda en menos de 100ms -- [ ] Validacion de VAT por pais -- [ ] Exportacion a CSV - ---- - -## Notas Tecnicas - -### Indice Recomendado - -```sql --- Busqueda rapida -CREATE INDEX idx_contacts_search ON contacts -USING gin(to_tsvector('spanish', name || ' ' || COALESCE(email, '') || ' ' || COALESCE(vat, ''))); - --- Filtro por tipo y rol -CREATE INDEX idx_contacts_type_roles ON contacts(contact_type, roles) WHERE deleted_at IS NULL; -``` - -### Computed Fields - -```sql --- display_name computed -CASE - WHEN parent_id IS NOT NULL THEN - (SELECT name FROM contacts WHERE id = parent_id) || ', ' || name - ELSE name -END AS display_name -``` - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-002.md b/docs/03-requerimientos/RF-catalogs/RF-CATALOG-002.md deleted file mode 100644 index cdde617..0000000 --- a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-002.md +++ /dev/null @@ -1,277 +0,0 @@ -# RF-CATALOG-002: Paises y Estados (Catalogo Global) - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-CATALOG-002 | -| **Modulo** | MGN-005 Catalogs | -| **Titulo** | Paises y Estados (Catalogo Global) | -| **Prioridad** | Alta | -| **Estado** | Draft | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe proporcionar un catalogo global de paises y sus subdivisiones (estados, provincias, departamentos) basado en estandares ISO. Este catalogo es compartido por todos los tenants y NO tiene `tenant_id`. - -**Referencia Odoo:** -- `res.country` (odoo/addons/base/models/res_country.py) -- `res.country.state` (mismo archivo) -- `res.country.group` (agrupaciones regionales) - ---- - -## Requisitos Funcionales - -### RF-CATALOG-002.1: Catalogo de Paises - -El sistema debe incluir todos los paises segun ISO 3166-1: - -| Campo | Tipo | Descripcion | Ejemplo | -|-------|------|-------------|---------| -| id | UUID | Identificador unico | auto | -| name | string | Nombre del pais (traducible) | "Mexico" | -| code | char(2) | Codigo ISO 3166-1 alpha-2 | "MX" | -| code3 | char(3) | Codigo ISO 3166-1 alpha-3 | "MEX" | -| numeric_code | int | Codigo ISO 3166-1 numerico | 484 | -| phone_code | int | Codigo telefonico internacional | 52 | -| currency_id | FK | Moneda oficial | UUID-MXN | -| flag_url | string | URL a imagen de bandera | computed | -| is_active | boolean | Disponible para seleccion | true | - -**Datos:** -- 249 paises segun ISO 3166-1 -- Precargados en migracion inicial - -### RF-CATALOG-002.2: Formato de Direccion por Pais - -Cada pais define su formato de direccion: - -``` -address_format: "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s" -``` - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| address_format | text | Template de direccion | -| name_position | enum | before/after address | -| state_required | boolean | Estado obligatorio | -| zip_required | boolean | CP obligatorio | -| vat_label | string | Etiqueta para VAT (RFC, NIT, RUT) | - -**Ejemplos de formato:** - -``` -Mexico: -"%(street)s\n%(street2)s\n%(zip)s %(city)s, %(state_code)s\n%(country_name)s" - -USA: -"%(street)s\n%(street2)s\n%(city)s, %(state_code)s %(zip)s\n%(country_name)s" - -Spain: -"%(street)s\n%(street2)s\n%(zip)s %(city)s (%(state_name)s)\n%(country_name)s" -``` - -### RF-CATALOG-002.3: Estados/Provincias - -Subdivisiones de paises segun ISO 3166-2: - -| Campo | Tipo | Descripcion | Ejemplo | -|-------|------|-------------|---------| -| id | UUID | Identificador unico | auto | -| country_id | FK | Pais padre | UUID-MX | -| name | string | Nombre del estado | "Jalisco" | -| code | string | Codigo ISO 3166-2 | "MX-JAL" | -| is_active | boolean | Disponible para seleccion | true | - -**Datos:** -- ~4,000 subdivisiones globales -- Precargados para paises principales (MX, US, ES, CO, AR, etc.) - -### RF-CATALOG-002.4: Grupos de Paises - -Agrupaciones regionales para configuraciones: - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| id | UUID | Identificador unico | -| name | string | Nombre del grupo | -| country_ids | M:N | Paises incluidos | - -**Grupos predefinidos:** -- EU (Union Europea) -- LATAM (Latinoamerica) -- NAFTA (Norteamerica) -- MERCOSUR -- CARICOM - ---- - -## Operaciones (Solo Lectura para Tenants) - -### Listar Paises - -```typescript -GET /api/v1/catalogs/countries?active=true&search=mex - -Response: -{ - "data": [ - { - "id": "uuid", - "name": "Mexico", - "code": "MX", - "phoneCode": 52, - "flagUrl": "/flags/mx.png", - "currencyId": "uuid-mxn" - } - ] -} -``` - -### Obtener Estados de un Pais - -```typescript -GET /api/v1/catalogs/countries/:code/states - -Response: -{ - "data": [ - { "id": "uuid", "name": "Aguascalientes", "code": "MX-AGU" }, - { "id": "uuid", "name": "Jalisco", "code": "MX-JAL" }, - ... - ] -} -``` - -### Busqueda de Paises - -```typescript -GET /api/v1/catalogs/countries/search?q=me - -// Busca en: name, code, code3 -Response: -[ - { "id": "uuid", "name": "Mexico", "code": "MX" }, - { "id": "uuid", "name": "Montenegro", "code": "ME" } -] -``` - ---- - -## Restricciones - -### Catalogo Global - Solo Lectura - -| Operacion | Permitido | Rol Requerido | -|-----------|-----------|---------------| -| Leer | Si | Cualquier usuario autenticado | -| Crear | No | - (Solo migracion) | -| Actualizar | No | - (Solo platform_admin) | -| Eliminar | No | - (Solo platform_admin) | - -**Justificacion:** Los datos de paises son estandares internacionales que no deben ser modificados por tenants individuales. - -### Sin RLS - -Las tablas de paises y estados NO tienen `tenant_id` ni RLS: - -```sql --- No tiene tenant_id -CREATE TABLE core_catalogs.countries ( - id UUID PRIMARY KEY, - name VARCHAR(100) NOT NULL, - code CHAR(2) NOT NULL UNIQUE, - ... -); - --- Sin RLS --- ALTER TABLE ... ENABLE ROW LEVEL SECURITY; (NO APLICAR) -``` - ---- - -## Seed de Datos - -### Fuentes de Datos - -| Datos | Fuente | Cantidad | -|-------|--------|----------| -| Paises | ISO 3166-1 | 249 | -| Estados MX | INEGI | 32 | -| Estados US | USPS | 50 + DC + territories | -| Estados ES | INE | 52 provincias | -| Otros | ISO 3166-2 | Variable | - -### Script de Carga - -```sql --- Paises principales (ejemplo) -INSERT INTO core_catalogs.countries (name, code, code3, numeric_code, phone_code, vat_label) VALUES -('Mexico', 'MX', 'MEX', 484, 52, 'RFC'), -('United States', 'US', 'USA', 840, 1, 'Tax ID'), -('Spain', 'ES', 'ESP', 724, 34, 'NIF'), -('Colombia', 'CO', 'COL', 170, 57, 'NIT'), -('Argentina', 'AR', 'ARG', 32, 54, 'CUIT'); - --- Estados Mexico (ejemplo) -INSERT INTO core_catalogs.states (country_id, name, code) VALUES -((SELECT id FROM countries WHERE code = 'MX'), 'Aguascalientes', 'MX-AGU'), -((SELECT id FROM countries WHERE code = 'MX'), 'Baja California', 'MX-BCN'), -... -``` - ---- - -## Casos de Prueba - -| ID | Escenario | Resultado Esperado | -|----|-----------|-------------------| -| TC-001 | Listar paises activos | 200+ paises | -| TC-002 | Obtener estados de Mexico | 32 estados | -| TC-003 | Buscar por codigo "MX" | Mexico encontrado | -| TC-004 | Buscar por nombre parcial | Resultados filtrados | -| TC-005 | Intentar crear pais (tenant) | 403 Forbidden | -| TC-006 | Pais inactivo no aparece en lista | Excluido | - ---- - -## Criterios de Aceptacion - -- [ ] 249 paises cargados segun ISO 3166-1 -- [ ] Estados cargados para paises principales -- [ ] Busqueda por nombre y codigo -- [ ] Formato de direccion por pais -- [ ] Solo lectura para tenants -- [ ] Banderas disponibles - ---- - -## Notas Tecnicas - -### Indices - -```sql --- Busqueda rapida -CREATE INDEX idx_countries_code ON core_catalogs.countries(code); -CREATE INDEX idx_countries_name ON core_catalogs.countries(name); -CREATE INDEX idx_states_country ON core_catalogs.states(country_id); -CREATE INDEX idx_states_code ON core_catalogs.states(code); -``` - -### Cache - -Los catalogos globales deben cachearse agresivamente: -- TTL: 24 horas -- Invalidacion: Manual por platform_admin - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-003.md b/docs/03-requerimientos/RF-catalogs/RF-CATALOG-003.md deleted file mode 100644 index f5198ab..0000000 --- a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-003.md +++ /dev/null @@ -1,320 +0,0 @@ -# RF-CATALOG-003: Monedas y Tasas de Cambio - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-CATALOG-003 | -| **Modulo** | MGN-005 Catalogs | -| **Titulo** | Monedas y Tasas de Cambio | -| **Prioridad** | Alta | -| **Estado** | Draft | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe proporcionar un catalogo de monedas (global) y tasas de cambio (por tenant) para soportar operaciones multi-moneda. - -**Referencia Odoo:** -- `res.currency` (odoo/addons/base/models/res_currency.py) -- `res.currency.rate` (mismo archivo) - ---- - -## Arquitectura de Monedas - -### Catalogo Global vs Por Tenant - -| Tabla | Scope | Descripcion | -|-------|-------|-------------| -| `core_catalogs.currencies` | Global | Lista ISO 4217 (sin tenant_id) | -| `core_catalogs.currency_rates` | Por Tenant | Tasas historicas (con tenant_id) | - -**Rationale:** -- Las monedas son estandares ISO, compartidas globalmente -- Las tasas de cambio varian por tenant (cada uno puede usar diferentes fuentes) - ---- - -## Requisitos Funcionales - -### RF-CATALOG-003.1: Catalogo de Monedas (Global) - -| Campo | Tipo | Descripcion | Ejemplo | -|-------|------|-------------|---------| -| id | UUID | Identificador unico | auto | -| name | char(3) | Codigo ISO 4217 | "MXN" | -| full_name | string | Nombre completo | "Mexican Peso" | -| symbol | string | Simbolo | "$" | -| iso_numeric | int | Codigo numerico ISO | 484 | -| decimal_places | int | Decimales | 2 | -| rounding | decimal | Factor de redondeo | 0.01 | -| position | enum | Posicion del simbolo | before/after | -| is_active | boolean | Disponible para uso | true | - -**Datos:** -- ~180 monedas segun ISO 4217 -- Precargadas en migracion inicial - -### RF-CATALOG-003.2: Tasas de Cambio (Por Tenant) - -Cada tenant mantiene su historial de tasas: - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| id | UUID | Identificador unico | -| tenant_id | FK | Tenant propietario | -| currency_id | FK | Moneda (global) | -| rate | decimal(20,10) | Tasa vs moneda base | -| inverse_rate | computed | 1/rate | -| name | date | Fecha de la tasa | -| created_at | timestamp | Fecha creacion | -| created_by | FK | Usuario que registro | - -**Reglas:** -- La moneda base del tenant tiene tasa = 1.0 -- Tasas se expresan como: 1 BASE = X MONEDA -- Se mantiene historial completo - -### RF-CATALOG-003.3: Moneda Base del Tenant - -Cada tenant tiene una moneda base definida en `tenant_settings`: - -```json -{ - "regional": { - "defaultCurrency": "MXN", - "currencyRateDate": "current" // current | invoice_date - } -} -``` - -**Reglas:** -- La moneda base NO puede cambiarse una vez hay transacciones -- Todas las tasas se calculan contra la moneda base - -### RF-CATALOG-003.4: Conversion de Monedas - -El sistema debe proporcionar funciones de conversion: - -```typescript -// Convertir monto de una moneda a otra -convertAmount( - amount: number, - fromCurrency: UUID, - toCurrency: UUID, - date?: Date // Default: hoy -): number - -// Obtener tasa de cambio -getRate( - fromCurrency: UUID, - toCurrency: UUID, - date?: Date -): number -``` - -**Logica de conversion:** -``` -amount_to = amount_from * (rate_to / rate_from) -``` - -### RF-CATALOG-003.5: Redondeo de Monedas - -Cada moneda define su precision: - -```typescript -// Redondear segun precision de moneda -roundCurrency(amount: number, currency: Currency): number - -// Ejemplo MXN (rounding = 0.01): -roundCurrency(123.456, MXN) // => 123.46 -``` - -### RF-CATALOG-003.6: Actualizacion Automatica de Tasas - -El sistema puede integrarse con APIs de tasas: - -| Proveedor | Endpoint | Frecuencia | -|-----------|----------|------------| -| Open Exchange Rates | api.openexchangerates.org | Diaria | -| Fixer.io | data.fixer.io | Diaria | -| Banco de Mexico | banxico.org.mx | Diaria | -| ECB | ecb.europa.eu | Diaria | - -**Configuracion por tenant:** -```json -{ - "regional": { - "currencyRateProvider": "banxico", - "currencyRateAutoUpdate": true - } -} -``` - ---- - -## Operaciones - -### Listar Monedas (Global) - -```typescript -GET /api/v1/catalogs/currencies?active=true - -Response: -{ - "data": [ - { - "id": "uuid", - "name": "MXN", - "fullName": "Mexican Peso", - "symbol": "$", - "decimalPlaces": 2, - "position": "before" - }, - ... - ] -} -``` - -### Obtener Tasas de un Tenant - -```typescript -GET /api/v1/currencies/rates?currency=MXN&from=2025-01-01&to=2025-01-31 - -Response: -{ - "data": [ - { "date": "2025-01-01", "rate": 17.2345 }, - { "date": "2025-01-02", "rate": 17.3021 }, - ... - ] -} -``` - -### Registrar Tasa Manual - -```typescript -POST /api/v1/currencies/rates -{ - "currencyId": "uuid-usd", - "rate": 17.5432, - "date": "2025-12-05" -} -``` - -### Convertir Monto - -```typescript -GET /api/v1/currencies/convert?from=USD&to=MXN&amount=100&date=2025-12-05 - -Response: -{ - "from": { "currency": "USD", "amount": 100 }, - "to": { "currency": "MXN", "amount": 1754.32 }, - "rate": 17.5432, - "date": "2025-12-05" -} -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Severidad | -|----|-------|-----------| -| BR-001 | Moneda base tasa = 1.0 siempre | Error | -| BR-002 | Tasa debe ser > 0 | Error | -| BR-003 | Una tasa por moneda por fecha | Error | -| BR-004 | No eliminar moneda con transacciones | Error | -| BR-005 | Moneda inactiva no seleccionable | Warning | - ---- - -## Casos de Prueba - -| ID | Escenario | Resultado Esperado | -|----|-----------|-------------------| -| TC-001 | Listar monedas activas | ~30 monedas comunes | -| TC-002 | Registrar tasa USD | Tasa guardada | -| TC-003 | Registrar tasa duplicada (misma fecha) | Error | -| TC-004 | Convertir 100 USD a MXN | Monto convertido | -| TC-005 | Convertir sin tasa del dia | Usar ultima tasa | -| TC-006 | Redondear MXN (2 decimales) | Precision correcta | -| TC-007 | Actualizar tasas automaticas | Tasas actualizadas | - ---- - -## Criterios de Aceptacion - -- [ ] ~180 monedas ISO 4217 cargadas -- [ ] Registro de tasas por tenant -- [ ] Conversion bidireccional -- [ ] Redondeo segun moneda -- [ ] Historial de tasas -- [ ] Integracion con proveedor externo (opcional) - ---- - -## Notas Tecnicas - -### Precision de Tasas - -```sql --- Alta precision para tasas -rate DECIMAL(20, 10) NOT NULL -``` - -**Rationale:** Algunas monedas tienen tasas muy pequenas (ej: 1 BTC = 0.000012 USD inverso) - -### Funcion de Conversion SQL - -```sql -CREATE OR REPLACE FUNCTION core_catalogs.convert_currency( - p_amount DECIMAL, - p_from_currency UUID, - p_to_currency UUID, - p_tenant_id UUID, - p_date DATE DEFAULT CURRENT_DATE -) RETURNS DECIMAL AS $$ -DECLARE - v_from_rate DECIMAL; - v_to_rate DECIMAL; -BEGIN - -- Obtener tasa de origen - SELECT rate INTO v_from_rate - FROM core_catalogs.currency_rates - WHERE tenant_id = p_tenant_id - AND currency_id = p_from_currency - AND name <= p_date - ORDER BY name DESC LIMIT 1; - - -- Obtener tasa de destino - SELECT rate INTO v_to_rate - FROM core_catalogs.currency_rates - WHERE tenant_id = p_tenant_id - AND currency_id = p_to_currency - AND name <= p_date - ORDER BY name DESC LIMIT 1; - - -- Convertir - RETURN p_amount * (COALESCE(v_to_rate, 1) / COALESCE(v_from_rate, 1)); -END; -$$ LANGUAGE plpgsql STABLE; -``` - -### Cache de Tasas - -- Tasas del dia: Cache 1 hora -- Tasas historicas: Cache 24 horas -- Invalidacion: Al registrar nueva tasa - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-004.md b/docs/03-requerimientos/RF-catalogs/RF-CATALOG-004.md deleted file mode 100644 index f6daf36..0000000 --- a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-004.md +++ /dev/null @@ -1,346 +0,0 @@ -# RF-CATALOG-004: Unidades de Medida (UoM) - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-CATALOG-004 | -| **Modulo** | MGN-005 Catalogs | -| **Titulo** | Unidades de Medida (UoM) | -| **Prioridad** | Media | -| **Estado** | Draft | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir gestionar unidades de medida agrupadas por categorias, con factores de conversion entre unidades de la misma categoria. - -**Referencia Odoo:** -- `uom.category` (addons/uom/models/uom_uom.py) -- `uom.uom` (mismo archivo) - ---- - -## Arquitectura - -### Por Tenant - -A diferencia de paises y monedas, las unidades de medida son **por tenant**: - -```sql -core_catalogs.uom_categories (tenant_id UUID NOT NULL) -core_catalogs.uom (tenant_id UUID NOT NULL) -``` - -**Rationale:** Cada tenant puede necesitar unidades personalizadas segun su industria. - ---- - -## Requisitos Funcionales - -### RF-CATALOG-004.1: Categorias de UoM - -Las unidades se agrupan en categorias. Solo se pueden convertir unidades de la misma categoria. - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| id | UUID | Identificador unico | -| tenant_id | FK | Tenant propietario | -| name | string | Nombre de categoria | -| is_active | boolean | Disponible para uso | - -**Categorias por defecto (seed):** - -| Categoria | Descripcion | -|-----------|-------------| -| Unit | Unidades discretas (piezas, docenas) | -| Weight | Peso (kg, g, lb, oz) | -| Volume | Volumen (L, mL, gal) | -| Length | Longitud (m, cm, ft, in) | -| Time | Tiempo (hora, dia, semana) | -| Area | Area (m2, ha, acre) | - -### RF-CATALOG-004.2: Unidades de Medida - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| id | UUID | Identificador unico | -| tenant_id | FK | Tenant propietario | -| category_id | FK | Categoria de UoM | -| name | string | Nombre de unidad | -| uom_type | enum | reference/bigger/smaller | -| factor | decimal | Factor de conversion | -| rounding | decimal | Precision de redondeo | -| is_active | boolean | Disponible para uso | - -### RF-CATALOG-004.3: Tipos de Unidad - -Cada categoria tiene exactamente UNA unidad de referencia: - -| Tipo | Descripcion | Factor | -|------|-------------|--------| -| `reference` | Unidad base de la categoria | 1.0 | -| `bigger` | Mayor que la referencia | < 1.0 | -| `smaller` | Menor que la referencia | > 1.0 | - -**Ejemplo categoria "Weight":** - -| Unidad | Tipo | Factor | Equivalencia | -|--------|------|--------|--------------| -| kg | reference | 1.0 | 1 kg = 1 kg | -| g | smaller | 1000 | 1 kg = 1000 g | -| lb | bigger | 0.453592 | 1 lb = 0.453592 kg | -| oz | smaller | 35.274 | 1 kg = 35.274 oz | - -### RF-CATALOG-004.4: Conversion de Unidades - -El sistema debe proporcionar funciones de conversion: - -```typescript -// Convertir cantidad de una unidad a otra -convertUom( - quantity: number, - fromUom: UUID, - toUom: UUID -): number - -// Redondear segun unidad -roundUom(quantity: number, uom: UUID): number -``` - -**Formula de conversion:** -``` -quantity_to = quantity_from * (factor_from / factor_to) -``` - -**Validacion:** Solo se permite conversion entre unidades de la misma categoria. - -### RF-CATALOG-004.5: Constraints de Categoria - -| Constraint | Descripcion | -|------------|-------------| -| Una referencia | Exactamente 1 unidad `reference` por categoria | -| Factor referencia = 1 | La unidad reference debe tener factor = 1.0 | -| Factor > 0 | El factor de conversion debe ser positivo | - -### RF-CATALOG-004.6: Unidades Protegidas - -Algunas unidades del seed no deben modificarse: - -```sql --- Unidades protegidas -is_protected BOOLEAN DEFAULT false -``` - -| Unidad | Categoria | Protegida | Razon | -|--------|-----------|-----------|-------| -| pcs | Unit | Si | Referencia universal | -| kg | Weight | Si | SI standard | -| L | Volume | Si | SI standard | -| m | Length | Si | SI standard | -| hr | Time | Si | Referencia de tiempo | - ---- - -## Operaciones CRUD - -### Listar Categorias - -```typescript -GET /api/v1/catalogs/uom-categories - -Response: -{ - "data": [ - { "id": "uuid", "name": "Unit", "uomCount": 3 }, - { "id": "uuid", "name": "Weight", "uomCount": 5 }, - ... - ] -} -``` - -### Listar Unidades por Categoria - -```typescript -GET /api/v1/catalogs/uom?categoryId=uuid-weight - -Response: -{ - "data": [ - { - "id": "uuid", - "name": "kg", - "uomType": "reference", - "factor": 1.0, - "rounding": 0.001 - }, - { - "id": "uuid", - "name": "g", - "uomType": "smaller", - "factor": 1000, - "rounding": 1 - } - ] -} -``` - -### Crear Unidad - -```typescript -POST /api/v1/catalogs/uom -{ - "categoryId": "uuid-weight", - "name": "Tonelada", - "uomType": "bigger", - "factor": 0.001, // 1 ton = 0.001 kg^-1 = 1000 kg - "rounding": 0.001 -} -``` - -### Convertir Cantidad - -```typescript -GET /api/v1/catalogs/uom/convert?from=uuid-kg&to=uuid-lb&quantity=10 - -Response: -{ - "from": { "uom": "kg", "quantity": 10 }, - "to": { "uom": "lb", "quantity": 22.0462 }, - "factor": 2.20462 -} -``` - ---- - -## Data Seed - -### Categoria: Unit - -| Nombre | Tipo | Factor | Rounding | -|--------|------|--------|----------| -| pcs (Pieces) | reference | 1 | 1 | -| dozen | bigger | 0.0833 | 1 | -| pair | bigger | 0.5 | 1 | - -### Categoria: Weight - -| Nombre | Tipo | Factor | Rounding | -|--------|------|--------|----------| -| kg | reference | 1 | 0.001 | -| g | smaller | 1000 | 1 | -| lb | bigger | 0.453592 | 0.01 | -| oz | smaller | 35.274 | 0.01 | -| ton | bigger | 0.001 | 0.001 | - -### Categoria: Volume - -| Nombre | Tipo | Factor | Rounding | -|--------|------|--------|----------| -| L | reference | 1 | 0.001 | -| mL | smaller | 1000 | 1 | -| gal (US) | bigger | 0.264172 | 0.001 | -| m3 | bigger | 0.001 | 0.001 | - -### Categoria: Length - -| Nombre | Tipo | Factor | Rounding | -|--------|------|--------|----------| -| m | reference | 1 | 0.001 | -| cm | smaller | 100 | 1 | -| ft | bigger | 0.3048 | 0.01 | -| in | smaller | 39.3701 | 0.1 | - -### Categoria: Time - -| Nombre | Tipo | Factor | Rounding | -|--------|------|--------|----------| -| hour | reference | 1 | 0.01 | -| day | bigger | 0.0417 | 0.01 | -| week | bigger | 0.00595 | 0.01 | - ---- - -## Reglas de Negocio - -| ID | Regla | Severidad | -|----|-------|-----------| -| BR-001 | Una unidad reference por categoria | Error | -| BR-002 | Reference factor = 1.0 | Error | -| BR-003 | Factor > 0 | Error | -| BR-004 | Solo convertir misma categoria | Error | -| BR-005 | No eliminar unidad con productos | Error | -| BR-006 | No modificar unidades protegidas | Error | - ---- - -## Casos de Prueba - -| ID | Escenario | Resultado Esperado | -|----|-----------|-------------------| -| TC-001 | Crear categoria nueva | Categoria creada | -| TC-002 | Crear unidad reference | Factor = 1.0 validado | -| TC-003 | Crear segunda reference | Error: ya existe | -| TC-004 | Convertir kg a lb | 10 kg = 22.0462 lb | -| TC-005 | Convertir kg a L (diferente categoria) | Error | -| TC-006 | Redondear 10.12345 kg (rounding=0.001) | 10.123 | -| TC-007 | Eliminar unidad con productos | Error | - ---- - -## Criterios de Aceptacion - -- [ ] Categorias CRUD funcional -- [ ] Unidades CRUD funcional -- [ ] Constraint de una reference -- [ ] Conversion dentro de categoria -- [ ] Redondeo segun precision -- [ ] Seed de unidades comunes -- [ ] Proteccion de unidades base - ---- - -## Notas Tecnicas - -### Funcion de Conversion SQL - -```sql -CREATE OR REPLACE FUNCTION core_catalogs.convert_uom( - p_quantity DECIMAL, - p_from_uom UUID, - p_to_uom UUID -) RETURNS DECIMAL AS $$ -DECLARE - v_from_factor DECIMAL; - v_to_factor DECIMAL; - v_from_category UUID; - v_to_category UUID; -BEGIN - -- Obtener datos de origen - SELECT factor, category_id INTO v_from_factor, v_from_category - FROM core_catalogs.uom WHERE id = p_from_uom; - - -- Obtener datos de destino - SELECT factor, category_id INTO v_to_factor, v_to_category - FROM core_catalogs.uom WHERE id = p_to_uom; - - -- Validar misma categoria - IF v_from_category != v_to_category THEN - RAISE EXCEPTION 'Cannot convert between different UoM categories'; - END IF; - - -- Convertir - RETURN p_quantity * (v_from_factor / v_to_factor); -END; -$$ LANGUAGE plpgsql STABLE; -``` - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-005.md b/docs/03-requerimientos/RF-catalogs/RF-CATALOG-005.md deleted file mode 100644 index 83feb5c..0000000 --- a/docs/03-requerimientos/RF-catalogs/RF-CATALOG-005.md +++ /dev/null @@ -1,348 +0,0 @@ -# RF-CATALOG-005: Categorias y Tags - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-CATALOG-005 | -| **Modulo** | MGN-005 Catalogs | -| **Titulo** | Categorias y Tags | -| **Prioridad** | Media | -| **Estado** | Draft | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe proporcionar un sistema flexible de categorias y tags para organizar contactos, productos y otros registros. Las categorias son jerarquicas (arbol) mientras que los tags son planos con colores. - -**Referencia Odoo:** -- `res.partner.category` (odoo/addons/base/models/res_partner.py) - ---- - -## Requisitos Funcionales - -### RF-CATALOG-005.1: Contact Tags - -Tags para clasificar contactos (clientes VIP, proveedores preferidos, etc.): - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| id | UUID | Identificador unico | -| tenant_id | FK | Tenant propietario | -| name | string | Nombre del tag | -| color | int | Color (1-11) | -| parent_id | FK | Tag padre (jerarquia) | -| is_active | boolean | Disponible para uso | - -**Jerarquia:** -- Tags pueden tener padre para organizacion -- Display name incluye path: "Clientes / VIP / Gold" - -### RF-CATALOG-005.2: Colores de Tags - -Sistema de colores predefinidos (similar a Odoo): - -| Color ID | Nombre | Hex | -|----------|--------|-----| -| 1 | Red | #F06050 | -| 2 | Orange | #F4A460 | -| 3 | Yellow | #F7CD1F | -| 4 | Light Blue | #6CC1ED | -| 5 | Dark Purple | #814968 | -| 6 | Salmon Pink | #EB7E7F | -| 7 | Medium Blue | #2C8397 | -| 8 | Dark Blue | #475577 | -| 9 | Fuchsia | #D6145F | -| 10 | Green | #30C381 | -| 11 | Purple | #9365B8 | - -### RF-CATALOG-005.3: Asignacion de Tags a Contactos - -Relacion muchos a muchos: - -```sql -core_catalogs.contact_tag_rel ( - contact_id UUID NOT NULL, - tag_id UUID NOT NULL, - PRIMARY KEY (contact_id, tag_id) -) -``` - -**Operaciones:** -- Agregar tag a contacto -- Remover tag de contacto -- Obtener contactos por tag -- Obtener tags de contacto - -### RF-CATALOG-005.4: Categorias Genericas - -Sistema extensible de categorias para diferentes entidades: - -| Campo | Tipo | Descripcion | -|-------|------|-------------| -| id | UUID | Identificador unico | -| tenant_id | FK | Tenant propietario | -| entity_type | string | Tipo de entidad (contact, product, etc.) | -| name | string | Nombre de categoria | -| code | string | Codigo unico por tipo | -| parent_id | FK | Categoria padre | -| sort_order | int | Orden de visualizacion | -| is_active | boolean | Disponible para uso | - -**Tipos de entidad soportados:** -- `contact` - Categorias de contactos -- `product` - Categorias de productos -- `expense` - Categorias de gastos -- `document` - Categorias de documentos - -### RF-CATALOG-005.5: Jerarquia de Categorias - -Implementacion con path materializado para consultas eficientes: - -```sql -parent_path VARCHAR(255) -- Ej: "1/5/12/" -``` - -**Funciones:** -- Obtener hijos directos -- Obtener todos los descendientes -- Obtener ancestros -- Mover categoria (cambiar parent) - -### RF-CATALOG-005.6: Display Name Computed - -El nombre mostrado incluye la jerarquia completa: - -``` -display_name = "Categoria Padre / Categoria Hijo / Esta Categoria" -``` - ---- - -## Operaciones CRUD - -### Crear Tag - -```typescript -POST /api/v1/catalogs/contact-tags -{ - "name": "Cliente VIP", - "color": 10, - "parentId": null -} -``` - -### Listar Tags - -```typescript -GET /api/v1/catalogs/contact-tags - -Response: -{ - "data": [ - { - "id": "uuid", - "name": "Cliente VIP", - "displayName": "Clientes / Cliente VIP", - "color": 10, - "contactsCount": 25 - } - ] -} -``` - -### Asignar Tags a Contacto - -```typescript -POST /api/v1/contacts/:id/tags -{ - "tagIds": ["uuid-tag-1", "uuid-tag-2"] -} -``` - -### Buscar Contactos por Tag - -```typescript -GET /api/v1/contacts?tags=uuid-tag-1,uuid-tag-2 - -// Devuelve contactos que tienen TODOS los tags especificados -``` - -### Crear Categoria - -```typescript -POST /api/v1/catalogs/categories -{ - "entityType": "product", - "name": "Electronicos", - "code": "ELEC", - "parentId": null -} -``` - -### Listar Categorias (Arbol) - -```typescript -GET /api/v1/catalogs/categories?entityType=product&format=tree - -Response: -{ - "data": [ - { - "id": "uuid", - "name": "Electronicos", - "code": "ELEC", - "children": [ - { - "id": "uuid", - "name": "Computadoras", - "code": "COMP", - "children": [] - }, - { - "id": "uuid", - "name": "Celulares", - "code": "CEL", - "children": [] - } - ] - } - ] -} -``` - ---- - -## Data Seed - -### Contact Tags por Defecto - -| Nombre | Color | Descripcion | -|--------|-------|-------------| -| Cliente VIP | 10 (green) | Clientes prioritarios | -| Proveedor Preferido | 7 (blue) | Proveedores de confianza | -| Prospecto | 3 (yellow) | Clientes potenciales | -| Inactivo | 8 (dark blue) | Contactos sin actividad | - -### Categorias de Producto por Defecto - -``` -Todos -├── Productos Vendibles -│ ├── Bienes -│ │ ├── Materias Primas -│ │ └── Productos Terminados -│ └── Servicios -├── Consumibles -└── Activos Fijos -``` - -### Categorias de Gasto por Defecto - -``` -Gastos -├── Operativos -│ ├── Nomina -│ ├── Servicios -│ └── Mantenimiento -├── Administrativos -│ ├── Papeleria -│ └── Comunicacion -└── Financieros - └── Intereses -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Severidad | -|----|-------|-----------| -| BR-001 | Nombre unico por parent_id | Error | -| BR-002 | Code unico por entity_type | Error | -| BR-003 | No ciclos en jerarquia | Error | -| BR-004 | Color entre 1-11 | Error | -| BR-005 | No eliminar con registros asociados | Warning | - ---- - -## Casos de Prueba - -| ID | Escenario | Resultado Esperado | -|----|-----------|-------------------| -| TC-001 | Crear tag con color | Tag creado | -| TC-002 | Crear tag hijo | display_name incluye padre | -| TC-003 | Asignar tag a contacto | Relacion creada | -| TC-004 | Buscar por tag | Contactos filtrados | -| TC-005 | Crear categoria jerarquica | parent_path calculado | -| TC-006 | Mover categoria | parent_path actualizado | -| TC-007 | Crear ciclo en jerarquia | Error | - ---- - -## Criterios de Aceptacion - -- [ ] CRUD de contact tags -- [ ] Sistema de colores funcional -- [ ] Asignacion M:N tags-contactos -- [ ] CRUD de categorias genericas -- [ ] Jerarquia con parent_path -- [ ] Display name computed -- [ ] Seed de datos iniciales - ---- - -## Notas Tecnicas - -### Indice para Jerarquia - -```sql --- Para consultas de descendientes -CREATE INDEX idx_categories_parent_path -ON core_catalogs.categories USING gin (parent_path gin_trgm_ops); - --- Para validar ciclos -CREATE OR REPLACE FUNCTION core_catalogs.check_category_cycle() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.parent_id IS NOT NULL AND - EXISTS ( - SELECT 1 FROM core_catalogs.categories - WHERE id = NEW.parent_id - AND parent_path LIKE '%/' || NEW.id || '/%' - ) THEN - RAISE EXCEPTION 'Cycle detected in category hierarchy'; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; -``` - -### Computed Display Name - -```sql -CREATE OR REPLACE FUNCTION core_catalogs.get_category_display_name(p_id UUID) -RETURNS TEXT AS $$ -WITH RECURSIVE ancestors AS ( - SELECT id, name, parent_id, 1 as level - FROM core_catalogs.categories WHERE id = p_id - UNION ALL - SELECT c.id, c.name, c.parent_id, a.level + 1 - FROM core_catalogs.categories c - JOIN ancestors a ON c.id = a.parent_id -) -SELECT string_agg(name, ' / ' ORDER BY level DESC) -FROM ancestors; -$$ LANGUAGE SQL STABLE; -``` - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-rbac/INDICE-RF-ROLE.md b/docs/03-requerimientos/RF-rbac/INDICE-RF-ROLE.md deleted file mode 100644 index f63ffb9..0000000 --- a/docs/03-requerimientos/RF-rbac/INDICE-RF-ROLE.md +++ /dev/null @@ -1,221 +0,0 @@ -# Indice de Requerimientos Funcionales - MGN-003 Roles/RBAC - -## Resumen del Modulo - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-003 | -| **Nombre** | Roles y RBAC | -| **Descripcion** | Control de acceso basado en roles | -| **Total RFs** | 4 | -| **Story Points** | 64 | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Lista de Requerimientos - -| ID | Nombre | Prioridad | SP | Estado | -|----|--------|-----------|-----|--------| -| [RF-ROLE-001](./RF-ROLE-001.md) | CRUD de Roles | P0 | 20 | Ready | -| [RF-ROLE-002](./RF-ROLE-002.md) | Gestion de Permisos | P0 | 12 | Ready | -| [RF-ROLE-003](./RF-ROLE-003.md) | Asignacion de Roles a Usuarios | P0 | 17 | Ready | -| [RF-ROLE-004](./RF-ROLE-004.md) | Guards y Middlewares RBAC | P0 | 15 | Ready | - ---- - -## Diagrama de Dependencias - -``` - ┌─────────────────┐ - │ RF-AUTH-001 │ - │ (Login/JWT) │ - └────────┬────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ MGN-003: Roles/RBAC │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ RF-ROLE-002 │◄─────────│ RF-ROLE-001 │ │ -│ │ Permisos │ │ CRUD Roles │ │ -│ └────────┬────────┘ └────────┬────────┘ │ -│ │ │ │ -│ │ ┌──────────────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌─────────────────────────────┐ │ -│ │ RF-ROLE-003 │ │ -│ │ Asignacion Roles-Usuarios │ │ -│ └─────────────┬───────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────┐ │ -│ │ RF-ROLE-004 │ │ -│ │ Guards y Middlewares │ │ -│ └─────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Todos los endpoints │ - │ del sistema usan RBAC │ - └──────────────────────────┘ -``` - ---- - -## Arquitectura del Sistema RBAC - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ RBAC Architecture │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │ -│ │ User │──────│ Role │──────│ Permission │ │ -│ └─────────┘ M:N └─────────┘ M:N └─────────────┘ │ -│ │ -│ Un usuario puede tener multiples roles │ -│ Un rol puede tener multiples permisos │ -│ Permisos efectivos = Union de permisos de todos los roles │ -│ │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ Formato de Permisos: modulo:accion │ -│ modulo:recurso:accion │ -│ │ -│ Wildcards: users:* (todas las acciones) │ -│ inventory:products:* │ -│ │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ Flujo de Validacion: │ -│ │ -│ Request → JwtGuard → TenantGuard → RbacGuard → Controller │ -│ │ │ -│ ▼ │ -│ ┌─────────────┐ │ -│ │ Cache │ │ -│ │ (5 min) │ │ -│ └─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Roles del Sistema (Built-in) - -| Rol | Slug | Permisos Base | Modificable | -|-----|------|---------------|-------------| -| Super Administrador | super_admin | Todos (*) | No | -| Administrador | admin | Gestion tenant | Solo extender | -| Gerente | manager | Lectura + reportes | Solo extender | -| Usuario | user | Acceso basico | Solo extender | -| Invitado | guest | Solo dashboard | Solo extender | - ---- - -## Catalogo de Permisos por Modulo - -### MGN-001 Auth (2 permisos) -- `auth:sessions:read` -- `auth:sessions:revoke` - -### MGN-002 Users (7 permisos) -- `users:read`, `users:create`, `users:update`, `users:delete` -- `users:activate`, `users:export`, `users:import` - -### MGN-003 Roles (6 permisos) -- `roles:read`, `roles:create`, `roles:update`, `roles:delete` -- `roles:assign`, `permissions:read` - -### MGN-004 Tenants (3 permisos) -- `tenants:read`, `tenants:update`, `tenants:billing` - -### MGN-006 Settings (2 permisos) -- `settings:read`, `settings:update` - -### MGN-007 Audit (2 permisos) -- `audit:read`, `audit:export` - -### MGN-009 Reports (4 permisos) -- `reports:read`, `reports:create`, `reports:export`, `reports:schedule` - -### MGN-010 Financial (6 permisos) -- `financial:accounts:read`, `financial:accounts:manage` -- `financial:transactions:read`, `financial:transactions:create`, `financial:transactions:approve` -- `financial:reports:read` - -### MGN-011 Inventory (8 permisos) -- `inventory:products:read`, `inventory:products:create`, `inventory:products:update`, `inventory:products:delete` -- `inventory:stock:read`, `inventory:stock:adjust` -- `inventory:movements:read`, `inventory:movements:create` - -**Total: ~40 permisos base** - ---- - -## Estimacion Total - -| Capa | Story Points | -|------|--------------| -| Backend: Endpoints | 15 | -| Backend: Guards/Decorators | 10 | -| Backend: Logica permisos | 8 | -| Backend: Cache | 4 | -| Backend: Tests | 10 | -| Frontend: RolesPage | 6 | -| Frontend: PermissionSelector | 4 | -| Frontend: RoleAssignment | 5 | -| Frontend: Tests | 6 | -| **Total** | **68 SP** | - -> Nota: Los 64 SP indicados en resumen corresponden a la suma de RF individuales. -> La estimacion detallada es de 68 SP incluyendo integracion. - ---- - -## Definition of Done del Modulo - -- [ ] RF-ROLE-001: CRUD de roles completo -- [ ] RF-ROLE-002: Catalogo de permisos seeded -- [ ] RF-ROLE-003: Asignacion roles-usuarios funcional -- [ ] RF-ROLE-004: Guards aplicados a todos los endpoints -- [ ] Cache de permisos implementado -- [ ] Tests unitarios > 80% coverage -- [ ] Tests e2e de flujos RBAC -- [ ] Documentacion Swagger completa -- [ ] Code review aprobado -- [ ] Security review aprobado - ---- - -## Notas de Implementacion - -### Orden Recomendado - -1. **Primero**: RF-ROLE-002 (Permisos) - Seed inicial de permisos -2. **Segundo**: RF-ROLE-001 (Roles) - CRUD de roles con permisos -3. **Tercero**: RF-ROLE-003 (Asignacion) - Vincular usuarios y roles -4. **Cuarto**: RF-ROLE-004 (Guards) - Proteger todos los endpoints - -### Consideraciones de Seguridad - -- Nunca revelar que permiso falta en errores 403 -- Logs de acceso denegado para auditoria -- Super Admin no debe poder eliminarse a si mismo -- Cache de permisos debe invalidarse al cambiar roles -- Validar permisos en CADA request (no confiar en frontend) - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 RFs | diff --git a/docs/03-requerimientos/RF-rbac/RF-ROLE-001.md b/docs/03-requerimientos/RF-rbac/RF-ROLE-001.md deleted file mode 100644 index 1f7f3cb..0000000 --- a/docs/03-requerimientos/RF-rbac/RF-ROLE-001.md +++ /dev/null @@ -1,364 +0,0 @@ -# RF-ROLE-001: CRUD de Roles - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-ROLE-001 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Prioridad** | P0 - Critica | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a los administradores crear, listar, ver, actualizar y eliminar roles dentro de su tenant. Los roles son contenedores de permisos que se asignan a usuarios para controlar su acceso a funcionalidades del sistema. - ---- - -## Actores - -| Actor | Descripcion | -|-------|-------------| -| Super Admin | Puede gestionar roles del sistema (built-in) | -| Admin | Puede crear y gestionar roles personalizados del tenant | - ---- - -## Precondiciones - -1. Usuario autenticado con permiso `roles:create`, `roles:read`, `roles:update` o `roles:delete` -2. Tenant activo -3. Para modificar: rol existente en el tenant - ---- - -## Flujo Principal - -### Crear Rol - -``` -1. Admin accede a Configuracion > Roles -2. Click en "Nuevo Rol" -3. Sistema muestra formulario: - - Nombre del rol (unico en tenant) - - Descripcion - - Seleccion de permisos -4. Admin completa datos y selecciona permisos -5. Click en "Crear Rol" -6. Sistema valida unicidad del nombre -7. Sistema crea el rol con los permisos seleccionados -8. Sistema muestra mensaje de exito -``` - -### Listar Roles - -``` -1. Admin accede a Configuracion > Roles -2. Sistema muestra tabla con: - - Nombre del rol - - Descripcion - - Numero de permisos - - Numero de usuarios asignados - - Es rol del sistema (built-in) - - Fecha de creacion -3. Admin puede filtrar por: - - Nombre - - Tipo (sistema/personalizado) -4. Admin puede ordenar por cualquier columna -``` - -### Ver Detalle de Rol - -``` -1. Admin click en un rol de la lista -2. Sistema muestra: - - Informacion basica del rol - - Lista de permisos asignados (agrupados por modulo) - - Lista de usuarios con este rol - - Historial de cambios -``` - -### Actualizar Rol - -``` -1. Admin click en "Editar" de un rol -2. Sistema muestra formulario con datos actuales -3. Admin modifica campos permitidos -4. Click en "Guardar Cambios" -5. Sistema valida cambios -6. Sistema actualiza el rol -7. Sistema registra en auditoria -``` - -### Eliminar Rol - -``` -1. Admin click en "Eliminar" de un rol -2. Sistema verifica si hay usuarios asignados -3. Si hay usuarios: - - Muestra advertencia con conteo - - Opcion de reasignar a otro rol -4. Admin confirma eliminacion -5. Sistema aplica soft delete -6. Sistema desasigna usuarios (si se confirmo) -``` - ---- - -## Flujos Alternativos - -### FA1: Nombre duplicado - -``` -1. En paso 6 del flujo crear -2. Sistema detecta nombre ya existe en tenant -3. Sistema muestra error "Ya existe un rol con este nombre" -4. Admin corrige el nombre -5. Continua desde paso 5 -``` - -### FA2: Intentar eliminar rol del sistema - -``` -1. Admin intenta eliminar rol built-in (admin, user, etc.) -2. Sistema muestra error "No se pueden eliminar roles del sistema" -3. Operacion cancelada -``` - -### FA3: Intentar modificar rol del sistema - -``` -1. Admin intenta modificar rol built-in -2. Sistema permite solo agregar permisos adicionales -3. No permite quitar permisos base ni cambiar nombre -``` - ---- - -## Reglas de Negocio - -| ID | Regla | -|----|-------| -| RN-001 | Nombre de rol unico dentro del tenant | -| RN-002 | Roles built-in no pueden eliminarse | -| RN-003 | Roles built-in solo pueden extenderse (agregar permisos) | -| RN-004 | Soft delete en lugar de hard delete | -| RN-005 | Rol debe tener al menos un permiso | -| RN-006 | Nombre del rol: 3-50 caracteres, alfanumerico con guiones | -| RN-007 | Al eliminar rol, usuarios quedan sin ese rol | -| RN-008 | Un tenant puede tener maximo 50 roles personalizados | - ---- - -## Roles del Sistema (Built-in) - -| Codigo | Nombre | Descripcion | Permisos Base | -|--------|--------|-------------|---------------| -| super_admin | Super Administrador | Acceso total al sistema | Todos | -| admin | Administrador | Gestion del tenant | Gestion usuarios, roles, config | -| manager | Gerente | Supervision operativa | Lectura + reportes | -| user | Usuario | Acceso basico | Solo lectura propia | -| guest | Invitado | Acceso minimo | Solo dashboard | - ---- - -## Criterios de Aceptacion - -### Escenario 1: Crear rol exitosamente - -```gherkin -Given un admin autenticado con permiso "roles:create" -When crea un rol con nombre "Vendedor" y 5 permisos -Then el sistema crea el rol - And el rol aparece en la lista - And el rol tiene isBuiltIn = false - And responde con status 201 -``` - -### Escenario 2: Listar roles con paginacion - -```gherkin -Given 25 roles en el tenant (5 built-in + 20 custom) -When el admin solicita GET /api/v1/roles?page=1&limit=10 -Then el sistema retorna 10 roles - And incluye meta con total=25, pages=3 - And roles built-in aparecen primero -``` - -### Escenario 3: No crear rol con nombre duplicado - -```gherkin -Given un rol existente con nombre "Vendedor" -When el admin intenta crear otro rol "Vendedor" -Then el sistema responde con status 409 - And el mensaje es "Ya existe un rol con este nombre" -``` - -### Escenario 4: No eliminar rol del sistema - -```gherkin -Given el rol built-in "admin" -When el admin intenta eliminarlo -Then el sistema responde con status 400 - And el mensaje es "No se pueden eliminar roles del sistema" -``` - -### Escenario 5: Soft delete de rol personalizado - -```gherkin -Given un rol personalizado "Vendedor" con 3 usuarios -When el admin lo elimina con reasignacion a "user" -Then el rol tiene deleted_at establecido - And los 3 usuarios ahora tienen rol "user" - And el rol no aparece en listados -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Roles [+ Nuevo Rol] | -+------------------------------------------------------------------+ -| Buscar: [___________________] [Tipo: Todos ▼] | -+------------------------------------------------------------------+ -| | Nombre | Descripcion | Permisos | Usuarios | ⚙ | -|---|-----------------|--------------------|-----------|---------|----| -| 🔒| Super Admin | Acceso total | 45 | 1 | 👁 | -| 🔒| Admin | Gestion tenant | 32 | 3 | 👁 | -| 🔒| Manager | Supervision | 18 | 5 | 👁 | -| 🔒| User | Acceso basico | 8 | 42 | 👁 | -| | Vendedor | Equipo de ventas | 12 | 8 | ✏🗑| -| | Contador | Area contable | 15 | 2 | ✏🗑| -| | Almacenista | Gestion inventario | 10 | 4 | ✏🗑| -+------------------------------------------------------------------+ -| Mostrando 1-7 de 7 [< Anterior] [Siguiente >]| -+------------------------------------------------------------------+ - -🔒 = Rol del sistema (built-in) - -Modal: Crear/Editar Rol -┌──────────────────────────────────────────────────────────────────┐ -│ NUEVO ROL │ -├──────────────────────────────────────────────────────────────────┤ -│ Nombre* [_______________________________] │ -│ Descripcion [_______________________________] │ -│ [_______________________________] │ -│ │ -│ PERMISOS [Seleccionar todos]│ -│ ┌────────────────────────────────────────────────────────────┐ │ -│ │ ▼ Usuarios (4 permisos) │ │ -│ │ ☑ users:read ☐ users:create │ │ -│ │ ☐ users:update ☐ users:delete │ │ -│ │ │ │ -│ │ ▼ Roles (4 permisos) │ │ -│ │ ☑ roles:read ☐ roles:create │ │ -│ │ ☐ roles:update ☐ roles:delete │ │ -│ │ │ │ -│ │ ▼ Inventario (6 permisos) │ │ -│ │ ☑ inventory:read ☑ inventory:create │ │ -│ │ ☑ inventory:update ☐ inventory:delete │ │ -│ │ ☑ inventory:export ☐ inventory:import │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ Permisos seleccionados: 7 │ -│ │ -│ [ Cancelar ] [ Crear Rol ] │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Crear rol -POST /api/v1/roles -{ - "name": "Vendedor", - "description": "Equipo de ventas", - "permissionIds": ["perm-uuid-1", "perm-uuid-2"] -} - -// Response 201 -{ - "id": "role-uuid", - "name": "Vendedor", - "slug": "vendedor", - "description": "Equipo de ventas", - "isBuiltIn": false, - "permissions": [...], - "usersCount": 0, - "createdAt": "2025-12-05T10:00:00Z" -} - -// Listar roles -GET /api/v1/roles?page=1&limit=10&search=vend&type=custom - -// Response 200 -{ - "data": [...], - "meta": { "total": 25, "page": 1, "limit": 10 } -} - -// Ver detalle -GET /api/v1/roles/:id - -// Actualizar rol -PATCH /api/v1/roles/:id -{ - "name": "Vendedor Senior", - "permissionIds": ["perm-1", "perm-2", "perm-3"] -} - -// Eliminar rol -DELETE /api/v1/roles/:id?reassignTo=other-role-id -``` - -### Validaciones - -| Campo | Validacion | -|-------|------------| -| name | Required, 3-50 chars, unique en tenant | -| slug | Auto-generado desde name | -| description | Optional, max 500 chars | -| permissionIds | Array min 1 elemento | - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-AUTH-001 | Login para autenticacion | -| RF-ROLE-002 | Permisos para asignar a roles | -| MGN-004 | Tenants para aislamiento | - ---- - -## Estimacion - -| Tarea | Puntos | -|-------|--------| -| Backend: CRUD endpoints | 5 | -| Backend: Validaciones y reglas | 3 | -| Backend: Tests | 2 | -| Frontend: RolesListPage | 3 | -| Frontend: RoleForm modal | 3 | -| Frontend: PermissionSelector | 2 | -| Frontend: Tests | 2 | -| **Total** | **20 SP** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-rbac/RF-ROLE-002.md b/docs/03-requerimientos/RF-rbac/RF-ROLE-002.md deleted file mode 100644 index 553da3a..0000000 --- a/docs/03-requerimientos/RF-rbac/RF-ROLE-002.md +++ /dev/null @@ -1,338 +0,0 @@ -# RF-ROLE-002: Gestion de Permisos - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-ROLE-002 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Prioridad** | P0 - Critica | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe mantener un catalogo de permisos que definen las acciones granulares que los usuarios pueden realizar. Los permisos se agrupan por modulo y se asignan a roles. El sistema soporta permisos jerarquicos donde un permiso padre implica todos sus hijos. - ---- - -## Actores - -| Actor | Descripcion | -|-------|-------------| -| Super Admin | Puede ver todos los permisos del sistema | -| Admin | Puede ver permisos disponibles para su tenant | -| Sistema | Registra nuevos permisos al instalar modulos | - ---- - -## Precondiciones - -1. Usuario autenticado con permiso `permissions:read` -2. Permisos base del sistema inicializados - ---- - -## Catalogo de Permisos - -### Estructura de Permisos - -Los permisos siguen el patron `modulo:accion` o `modulo:recurso:accion`: - -``` -users:read - Leer usuarios -users:create - Crear usuarios -users:update - Actualizar usuarios -users:delete - Eliminar usuarios -users:* - Todos los permisos de usuarios (wildcard) - -inventory:products:read - Leer productos -inventory:products:create - Crear productos -inventory:* - Todo el modulo inventario -``` - -### Permisos por Modulo - -#### Modulo Auth (MGN-001) - -| Permiso | Descripcion | -|---------|-------------| -| auth:sessions:read | Ver sesiones activas | -| auth:sessions:revoke | Revocar sesiones | - -#### Modulo Users (MGN-002) - -| Permiso | Descripcion | -|---------|-------------| -| users:read | Listar y ver usuarios | -| users:create | Crear usuarios | -| users:update | Actualizar usuarios | -| users:delete | Eliminar usuarios | -| users:activate | Activar/desactivar usuarios | -| users:export | Exportar lista de usuarios | -| users:import | Importar usuarios | - -#### Modulo Roles (MGN-003) - -| Permiso | Descripcion | -|---------|-------------| -| roles:read | Listar y ver roles | -| roles:create | Crear roles | -| roles:update | Actualizar roles | -| roles:delete | Eliminar roles | -| roles:assign | Asignar roles a usuarios | -| permissions:read | Ver permisos disponibles | - -#### Modulo Tenants (MGN-004) - -| Permiso | Descripcion | -|---------|-------------| -| tenants:read | Ver configuracion del tenant | -| tenants:update | Actualizar configuracion | -| tenants:billing | Gestionar facturacion | - -#### Modulo Settings (MGN-006) - -| Permiso | Descripcion | -|---------|-------------| -| settings:read | Ver configuracion | -| settings:update | Modificar configuracion | - -#### Modulo Audit (MGN-007) - -| Permiso | Descripcion | -|---------|-------------| -| audit:read | Ver logs de auditoria | -| audit:export | Exportar logs | - -#### Modulo Reports (MGN-009) - -| Permiso | Descripcion | -|---------|-------------| -| reports:read | Ver reportes | -| reports:create | Crear reportes personalizados | -| reports:export | Exportar reportes | -| reports:schedule | Programar reportes | - -#### Modulo Financial (MGN-010) - -| Permiso | Descripcion | -|---------|-------------| -| financial:accounts:read | Ver cuentas contables | -| financial:accounts:manage | Gestionar cuentas | -| financial:transactions:read | Ver transacciones | -| financial:transactions:create | Crear transacciones | -| financial:transactions:approve | Aprobar transacciones | -| financial:reports:read | Ver reportes financieros | - -#### Modulo Inventory (MGN-011) - -| Permiso | Descripcion | -|---------|-------------| -| inventory:products:read | Ver productos | -| inventory:products:create | Crear productos | -| inventory:products:update | Actualizar productos | -| inventory:products:delete | Eliminar productos | -| inventory:stock:read | Ver stock | -| inventory:stock:adjust | Ajustar stock | -| inventory:movements:read | Ver movimientos | -| inventory:movements:create | Crear movimientos | - ---- - -## Flujo Principal - -### Listar Permisos Disponibles - -``` -1. Admin accede a Configuracion > Roles > Nuevo/Editar -2. Sistema carga lista de permisos agrupados por modulo -3. Sistema muestra solo permisos habilitados para el tenant -4. Admin puede expandir/colapsar grupos -5. Admin puede buscar permisos por nombre -``` - -### Ver Permisos de un Rol - -``` -1. Admin accede a detalle de un rol -2. Sistema muestra permisos agrupados por modulo -3. Sistema indica cuales son heredados vs directos -4. Sistema muestra descripcion de cada permiso -``` - ---- - -## Reglas de Negocio - -| ID | Regla | -|----|-------| -| RN-001 | Permisos son inmutables por usuarios | -| RN-002 | Permisos se registran por el sistema al instalar modulos | -| RN-003 | Permiso wildcard (*) incluye todas las acciones del modulo | -| RN-004 | Tenant solo ve permisos de modulos que tiene activos | -| RN-005 | Codigo de permiso unico globalmente | -| RN-006 | Permisos pueden estar activos o deprecados | - ---- - -## Jerarquia de Permisos - -``` -users:* (Wildcard - todos los permisos de users) -├── users:read -├── users:create -├── users:update -├── users:delete -├── users:activate -├── users:export -└── users:import - -inventory:* (Wildcard - todo inventario) -├── inventory:products:* (Wildcard - productos) -│ ├── inventory:products:read -│ ├── inventory:products:create -│ ├── inventory:products:update -│ └── inventory:products:delete -├── inventory:stock:* -│ ├── inventory:stock:read -│ └── inventory:stock:adjust -└── inventory:movements:* - ├── inventory:movements:read - └── inventory:movements:create -``` - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar permisos disponibles - -```gherkin -Given un admin autenticado -When solicita GET /api/v1/permissions -Then el sistema retorna permisos agrupados por modulo - And cada permiso incluye code, name, description - And solo incluye permisos de modulos activos en el tenant -``` - -### Escenario 2: Buscar permisos - -```gherkin -Given lista de 50 permisos -When el admin busca "users" -Then el sistema retorna solo permisos que contienen "users" - And mantiene agrupacion por modulo -``` - -### Escenario 3: Validar wildcard - -```gherkin -Given un rol con permiso "users:*" -When se verifica si tiene "users:delete" -Then el sistema confirma que SI tiene el permiso - And el permiso es heredado (no directo) -``` - -### Escenario 4: Permisos por modulo deshabilitado - -```gherkin -Given tenant sin modulo de inventario activo -When admin lista permisos disponibles -Then NO aparecen permisos de "inventory:*" -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Listar todos los permisos -GET /api/v1/permissions -// Query params: ?search=users&module=users - -// Response 200 -{ - "data": [ - { - "module": "users", - "moduleName": "Gestion de Usuarios", - "permissions": [ - { - "id": "perm-uuid", - "code": "users:read", - "name": "Leer usuarios", - "description": "Permite ver listado y detalle de usuarios", - "isDeprecated": false - }, - // ... - ] - }, - { - "module": "roles", - "moduleName": "Roles y Permisos", - "permissions": [...] - } - ] -} - -// Listar permisos de un rol -GET /api/v1/roles/:roleId/permissions - -// Response 200 -{ - "direct": ["users:read", "users:create"], - "inherited": ["users:update"], // via wildcard - "all": ["users:read", "users:create", "users:update"] -} -``` - -### Modelo de Datos - -```typescript -interface Permission { - id: string; - code: string; // users:read - name: string; // Leer usuarios - description: string; - module: string; // users - parentCode?: string; // users:* (para jerarquia) - isDeprecated: boolean; - createdAt: Date; -} -``` - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-ROLE-001 | Roles que contienen permisos | -| MGN-004 | Tenants para filtrar por modulos activos | - ---- - -## Estimacion - -| Tarea | Puntos | -|-------|--------| -| Backend: Seed de permisos | 2 | -| Backend: Endpoint listar | 2 | -| Backend: Logica wildcard | 3 | -| Backend: Tests | 2 | -| Frontend: PermissionsList component | 2 | -| Frontend: Tests | 1 | -| **Total** | **12 SP** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-rbac/RF-ROLE-003.md b/docs/03-requerimientos/RF-rbac/RF-ROLE-003.md deleted file mode 100644 index c59b9e9..0000000 --- a/docs/03-requerimientos/RF-rbac/RF-ROLE-003.md +++ /dev/null @@ -1,350 +0,0 @@ -# RF-ROLE-003: Asignacion de Roles a Usuarios - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-ROLE-003 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Prioridad** | P0 - Critica | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir asignar uno o mas roles a cada usuario. Los permisos efectivos de un usuario son la union de todos los permisos de sus roles asignados. La asignacion puede realizarse desde la gestion de usuarios o desde la gestion de roles. - ---- - -## Actores - -| Actor | Descripcion | -|-------|-------------| -| Super Admin | Puede asignar cualquier rol a cualquier usuario | -| Admin | Puede asignar roles (excepto super_admin) a usuarios del tenant | - ---- - -## Precondiciones - -1. Usuario autenticado con permiso `roles:assign` -2. Usuario destino existente y activo -3. Rol existente y activo - ---- - -## Flujo Principal - -### Asignar Rol desde Usuario - -``` -1. Admin accede a Usuarios > Detalle de usuario -2. Click en "Gestionar Roles" -3. Sistema muestra: - - Roles actuales del usuario - - Roles disponibles para asignar -4. Admin selecciona roles a asignar -5. Admin deselecciona roles a quitar -6. Click en "Guardar Cambios" -7. Sistema actualiza asignaciones -8. Sistema recalcula permisos del usuario -9. Sistema muestra confirmacion -``` - -### Asignar Usuarios desde Rol - -``` -1. Admin accede a Roles > Detalle de rol -2. Click en pestaña "Usuarios" -3. Sistema muestra usuarios con este rol -4. Click en "Agregar Usuarios" -5. Sistema muestra lista de usuarios sin este rol -6. Admin selecciona usuarios -7. Click en "Asignar" -8. Sistema asigna rol a usuarios seleccionados -``` - -### Asignacion Masiva - -``` -1. Admin accede a Usuarios -2. Selecciona multiples usuarios (checkbox) -3. Click en "Acciones > Asignar Rol" -4. Sistema muestra selector de rol -5. Admin selecciona rol -6. Sistema asigna rol a todos los usuarios seleccionados -``` - ---- - -## Flujos Alternativos - -### FA1: Usuario ya tiene el rol - -``` -1. Admin intenta asignar rol que usuario ya tiene -2. Sistema ignora la asignacion duplicada -3. Continua sin error -``` - -### FA2: Quitar ultimo rol - -``` -1. Admin intenta quitar el unico rol del usuario -2. Sistema muestra advertencia -3. Admin confirma o cancela -4. Si confirma: usuario queda sin roles (acceso minimo) -``` - -### FA3: Admin intenta asignar super_admin - -``` -1. Admin (no super_admin) intenta asignar rol super_admin -2. Sistema muestra error "Solo Super Admin puede asignar este rol" -3. Operacion cancelada -``` - ---- - -## Reglas de Negocio - -| ID | Regla | -|----|-------| -| RN-001 | Usuario puede tener multiples roles | -| RN-002 | Permisos efectivos = union de permisos de todos los roles | -| RN-003 | Solo super_admin puede asignar rol super_admin | -| RN-004 | No se puede quitar rol super_admin del ultimo super_admin | -| RN-005 | Cambios de roles toman efecto inmediato | -| RN-006 | Se registra en auditoria cada cambio de asignacion | -| RN-007 | Usuario sin roles tiene acceso minimo (solo perfil propio) | - ---- - -## Criterios de Aceptacion - -### Escenario 1: Asignar rol a usuario - -```gherkin -Given un usuario "juan@empresa.com" con rol "user" - And un rol "vendedor" disponible -When el admin asigna rol "vendedor" al usuario -Then el usuario tiene roles ["user", "vendedor"] - And el usuario tiene permisos de ambos roles combinados - And se registra en auditoria -``` - -### Escenario 2: Quitar rol de usuario - -```gherkin -Given un usuario con roles ["admin", "manager"] -When el admin quita el rol "manager" -Then el usuario solo tiene rol ["admin"] - And pierde los permisos exclusivos de "manager" -``` - -### Escenario 3: Asignacion masiva - -```gherkin -Given 10 usuarios seleccionados - And rol "vendedor" disponible -When el admin asigna "vendedor" a todos -Then los 10 usuarios tienen rol "vendedor" agregado - And responde con resumen: { success: 10, failed: 0 } -``` - -### Escenario 4: Proteccion de super_admin - -```gherkin -Given un admin autenticado (no super_admin) -When intenta asignar rol "super_admin" a un usuario -Then el sistema responde con status 403 - And el mensaje es "Solo Super Admin puede asignar este rol" -``` - -### Escenario 5: No quitar ultimo super_admin - -```gherkin -Given solo 1 usuario con rol "super_admin" -When se intenta quitar el rol -Then el sistema responde con status 400 - And el mensaje es "Debe existir al menos un Super Admin" -``` - ---- - -## Mockup / Wireframe - -``` -Modal: Gestionar Roles de Usuario -┌──────────────────────────────────────────────────────────────────┐ -│ ROLES DE: Juan Perez (juan@empresa.com) │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ Roles Asignados: │ -│ ┌────────────────────────────────────────────────────────────┐ │ -│ │ ☑ Admin Gestion completa del tenant [x] │ │ -│ │ ☑ Manager Supervision operativa [x] │ │ -│ │ ☐ User Acceso basico │ │ -│ │ ☐ Vendedor Equipo de ventas │ │ -│ │ ☐ Contador Area contable │ │ -│ │ ☐ Almacenista Gestion de inventario │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ Permisos Efectivos: 45 permisos (de 2 roles) │ -│ [Ver detalle de permisos] │ -│ │ -│ [ Cancelar ] [ Guardar Cambios ] │ -└──────────────────────────────────────────────────────────────────┘ - -Vista: Usuarios de un Rol -┌──────────────────────────────────────────────────────────────────┐ -│ ROL: Vendedor [+ Agregar Usuarios]│ -├──────────────────────────────────────────────────────────────────┤ -│ Usuarios con este rol (8): │ -│ │ -│ | Avatar | Nombre | Email | Desde | ⚙ | -│ |--------|----------------|--------------------|-----------|----| -│ | 👤 | Carlos Lopez | carlos@empresa.com | 01/12/2025 | 🗑| -│ | 👤 | Maria Garcia | maria@empresa.com | 15/11/2025 | 🗑| -│ | 👤 | Pedro Martinez | pedro@empresa.com | 10/11/2025 | 🗑| -│ | ... | ... | ... | ... | ...| -│ │ -│ Mostrando 1-8 de 8 │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Asignar roles a usuario -PUT /api/v1/users/:userId/roles -{ - "roleIds": ["role-uuid-1", "role-uuid-2"] -} - -// Response 200 -{ - "userId": "user-uuid", - "roles": [ - { "id": "role-uuid-1", "name": "Admin", "assignedAt": "..." }, - { "id": "role-uuid-2", "name": "Manager", "assignedAt": "..." } - ], - "effectivePermissions": ["users:*", "reports:read", ...] -} - -// Agregar rol a usuario (sin quitar existentes) -POST /api/v1/users/:userId/roles -{ - "roleId": "role-uuid" -} - -// Quitar rol de usuario -DELETE /api/v1/users/:userId/roles/:roleId - -// Asignar rol a multiples usuarios -POST /api/v1/roles/:roleId/users -{ - "userIds": ["user-1", "user-2", "user-3"] -} - -// Response 200 -{ - "roleId": "role-uuid", - "results": { - "success": ["user-1", "user-2"], - "failed": [{ "userId": "user-3", "reason": "Ya tiene el rol" }] - } -} - -// Listar usuarios de un rol -GET /api/v1/roles/:roleId/users?page=1&limit=20 - -// Obtener permisos efectivos de usuario -GET /api/v1/users/:userId/permissions - -// Response 200 -{ - "roles": ["admin", "manager"], - "permissions": { - "direct": [...], // Permisos de roles asignados - "inherited": [...], // Via wildcards - "all": [...] // Union de todos - } -} -``` - -### Calculo de Permisos Efectivos - -```typescript -function calculateEffectivePermissions(userId: string): string[] { - // 1. Obtener roles del usuario - const userRoles = await getUserRoles(userId); - - // 2. Obtener permisos de cada rol - const allPermissions = new Set(); - - for (const role of userRoles) { - const rolePermissions = await getRolePermissions(role.id); - rolePermissions.forEach(p => allPermissions.add(p)); - } - - // 3. Expandir wildcards - const expanded = expandWildcards(allPermissions); - - return Array.from(expanded); -} - -function expandWildcards(permissions: Set): Set { - const result = new Set(permissions); - - for (const perm of permissions) { - if (perm.endsWith(':*')) { - // users:* -> agregar users:read, users:create, etc. - const module = perm.replace(':*', ''); - const modulePerms = getModulePermissions(module); - modulePerms.forEach(p => result.add(p)); - } - } - - return result; -} -``` - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-ROLE-001 | Roles existentes para asignar | -| RF-ROLE-002 | Permisos para calcular efectivos | -| RF-USER-001 | Usuarios existentes | - ---- - -## Estimacion - -| Tarea | Puntos | -|-------|--------| -| Backend: Endpoints asignacion | 3 | -| Backend: Calculo permisos efectivos | 3 | -| Backend: Validaciones y reglas | 2 | -| Backend: Tests | 2 | -| Frontend: RoleAssignment modal | 3 | -| Frontend: BulkAssignment | 2 | -| Frontend: Tests | 2 | -| **Total** | **17 SP** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-rbac/RF-ROLE-004.md b/docs/03-requerimientos/RF-rbac/RF-ROLE-004.md deleted file mode 100644 index 08f9797..0000000 --- a/docs/03-requerimientos/RF-rbac/RF-ROLE-004.md +++ /dev/null @@ -1,530 +0,0 @@ -# RF-ROLE-004: Guards y Middlewares RBAC - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-ROLE-004 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Prioridad** | P0 - Critica | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe implementar mecanismos de control de acceso basado en roles (RBAC) que se apliquen automaticamente a todos los endpoints protegidos. Esto incluye guards de NestJS, decoradores personalizados y middlewares para validar permisos antes de ejecutar cualquier accion. - ---- - -## Actores - -| Actor | Descripcion | -|-------|-------------| -| Sistema | Valida permisos en cada request | -| Usuario | Sujeto de validacion de permisos | - ---- - -## Precondiciones - -1. Usuario autenticado (JWT valido) -2. Roles y permisos cargados en el sistema -3. Endpoint decorado con permisos requeridos - ---- - -## Arquitectura RBAC - -``` -Request HTTP - │ - ▼ -┌─────────────────┐ -│ JwtAuthGuard │ Valida token JWT -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ TenantGuard │ Verifica tenant del usuario -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ RbacGuard │ Valida permisos requeridos -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ Controller │ Ejecuta logica de negocio -└─────────────────┘ -``` - ---- - -## Flujo de Validacion - -### Validacion Estandar - -``` -1. Request llega al servidor -2. JwtAuthGuard extrae y valida token -3. Sistema carga usuario y sus roles desde cache/DB -4. TenantGuard verifica que usuario pertenece al tenant -5. RbacGuard obtiene permisos requeridos del decorator -6. Sistema calcula permisos efectivos del usuario -7. Sistema verifica si tiene TODOS los permisos requeridos -8. Si tiene permisos: continua al controller -9. Si no tiene: retorna 403 Forbidden -``` - -### Validacion con Permisos Alternativos (OR) - -``` -1. Endpoint requiere: users:update OR users:admin -2. Usuario tiene: users:update -3. Sistema verifica si tiene AL MENOS UNO -4. Usuario tiene users:update -> acceso permitido -``` - -### Validacion Condicional (Owner) - -``` -1. Endpoint requiere: users:update OR ser owner del recurso -2. Sistema verifica permisos -3. Si no tiene permiso, verifica si es owner -4. Si es owner del recurso: acceso permitido -``` - ---- - -## Componentes del Sistema - -### 1. Decoradores - -```typescript -// Permiso requerido (AND) -@Permissions('users:read', 'users:update') -// Usuario debe tener AMBOS permisos - -// Permiso alternativo (OR) -@AnyPermission('users:update', 'users:admin') -// Usuario debe tener AL MENOS UNO - -// Rol requerido -@Roles('admin', 'manager') -// Usuario debe tener AL MENOS UNO de los roles - -// Acceso publico (sin auth) -@Public() - -// Owner o permiso -@OwnerOrPermission('users:update') -``` - -### 2. Guards - -```typescript -// JwtAuthGuard - Ya implementado en MGN-001 -// Valida token, extrae usuario - -// TenantGuard -// Verifica tenant_id del usuario vs tenant del recurso - -// RbacGuard -// Valida permisos segun decoradores -``` - -### 3. Interceptors - -```typescript -// PermissionInterceptor -// Agrega permisos efectivos al request para uso en controller - -// AuditInterceptor -// Registra acciones sensibles con info de permisos -``` - ---- - -## Reglas de Negocio - -| ID | Regla | -|----|-------| -| RN-001 | Permisos se validan en CADA request (no solo al login) | -| RN-002 | Permisos efectivos se cachean por sesion (TTL 5 min) | -| RN-003 | Cambios de roles invalidan cache de permisos | -| RN-004 | Super Admin bypasea validacion de permisos | -| RN-005 | Endpoints publicos no requieren autenticacion | -| RN-006 | Error 403 no revela que permisos faltan (seguridad) | -| RN-007 | Logs de acceso denegado para auditoria | - ---- - -## Criterios de Aceptacion - -### Escenario 1: Acceso con permiso valido - -```gherkin -Given un usuario con permiso "users:read" - And endpoint GET /api/v1/users requiere "users:read" -When el usuario hace la solicitud -Then el sistema permite el acceso - And retorna status 200 con los datos -``` - -### Escenario 2: Acceso denegado por falta de permiso - -```gherkin -Given un usuario con permiso "users:read" solamente - And endpoint DELETE /api/v1/users/:id requiere "users:delete" -When el usuario intenta eliminar -Then el sistema retorna status 403 - And el mensaje es "No tienes permiso para realizar esta accion" - And NO revela que permiso falta -``` - -### Escenario 3: Wildcard permite acceso - -```gherkin -Given un usuario con permiso "users:*" - And endpoint requiere "users:delete" -When el usuario hace la solicitud -Then el sistema permite el acceso - And wildcard cubre el permiso especifico -``` - -### Escenario 4: Super Admin bypass - -```gherkin -Given un usuario con rol "super_admin" - And endpoint requiere "cualquier:permiso" -When el usuario hace la solicitud -Then el sistema permite el acceso - And no valida permisos especificos -``` - -### Escenario 5: Permisos alternativos (OR) - -```gherkin -Given endpoint requiere "users:update" OR "users:admin" - And usuario tiene solo "users:admin" -When el usuario hace la solicitud -Then el sistema permite el acceso -``` - -### Escenario 6: Owner puede acceder - -```gherkin -Given endpoint PATCH /api/v1/users/:id con @OwnerOrPermission('users:update') - And usuario es owner del recurso (su propio perfil) - And usuario NO tiene permiso "users:update" -When el usuario actualiza su perfil -Then el sistema permite el acceso - And aplica validaciones de owner -``` - -### Escenario 7: Cache de permisos - -```gherkin -Given permisos de usuario cacheados -When el admin cambia roles del usuario -Then el cache se invalida - And siguiente request recalcula permisos -``` - ---- - -## Notas Tecnicas - -### Implementacion de Decoradores - -```typescript -// decorators/permissions.decorator.ts -import { SetMetadata } from '@nestjs/common'; - -export const PERMISSIONS_KEY = 'permissions'; -export const Permissions = (...permissions: string[]) => - SetMetadata(PERMISSIONS_KEY, { permissions, mode: 'AND' }); - -export const AnyPermission = (...permissions: string[]) => - SetMetadata(PERMISSIONS_KEY, { permissions, mode: 'OR' }); - -export const ROLES_KEY = 'roles'; -export const Roles = (...roles: string[]) => - SetMetadata(ROLES_KEY, roles); - -export const IS_PUBLIC_KEY = 'isPublic'; -export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); - -export const OWNER_OR_PERMISSION_KEY = 'ownerOrPermission'; -export const OwnerOrPermission = (permission: string) => - SetMetadata(OWNER_OR_PERMISSION_KEY, permission); -``` - -### Implementacion de RbacGuard - -```typescript -// guards/rbac.guard.ts -@Injectable() -export class RbacGuard implements CanActivate { - constructor( - private reflector: Reflector, - private permissionService: PermissionService, - private cacheManager: Cache, - ) {} - - async canActivate(context: ExecutionContext): Promise { - // 1. Verificar si es ruta publica - const isPublic = this.reflector.getAllAndOverride( - IS_PUBLIC_KEY, - [context.getHandler(), context.getClass()], - ); - if (isPublic) return true; - - // 2. Obtener usuario del request - const request = context.switchToHttp().getRequest(); - const user = request.user; - if (!user) return false; - - // 3. Super Admin bypass - if (user.roles.includes('super_admin')) return true; - - // 4. Obtener permisos requeridos - const requiredPermissions = this.reflector.getAllAndOverride<{ - permissions: string[]; - mode: 'AND' | 'OR'; - }>(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]); - - if (!requiredPermissions) return true; // No requiere permisos - - // 5. Obtener permisos efectivos (con cache) - const effectivePermissions = await this.getEffectivePermissions(user.id); - - // 6. Validar permisos - const { permissions, mode } = requiredPermissions; - - if (mode === 'AND') { - return permissions.every(p => this.hasPermission(effectivePermissions, p)); - } else { - return permissions.some(p => this.hasPermission(effectivePermissions, p)); - } - } - - private hasPermission(userPerms: string[], required: string): boolean { - // Verificar permiso directo - if (userPerms.includes(required)) return true; - - // Verificar wildcard - const [module] = required.split(':'); - if (userPerms.includes(`${module}:*`)) return true; - - // Verificar wildcard de segundo nivel - const parts = required.split(':'); - if (parts.length === 3) { - if (userPerms.includes(`${parts[0]}:${parts[1]}:*`)) return true; - } - - return false; - } - - private async getEffectivePermissions(userId: string): Promise { - const cacheKey = `permissions:${userId}`; - - // Intentar desde cache - const cached = await this.cacheManager.get(cacheKey); - if (cached) return cached; - - // Calcular permisos - const permissions = await this.permissionService.calculateEffective(userId); - - // Guardar en cache (5 minutos) - await this.cacheManager.set(cacheKey, permissions, 300000); - - return permissions; - } -} -``` - -### Implementacion de OwnerGuard - -```typescript -// guards/owner.guard.ts -@Injectable() -export class OwnerGuard implements CanActivate { - constructor( - private reflector: Reflector, - private permissionService: PermissionService, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const ownerPermission = this.reflector.get( - OWNER_OR_PERMISSION_KEY, - context.getHandler(), - ); - - if (!ownerPermission) return true; - - const request = context.switchToHttp().getRequest(); - const user = request.user; - const resourceId = request.params.id; - - // Verificar si tiene permiso - const hasPermission = await this.permissionService.userHas( - user.id, - ownerPermission, - ); - if (hasPermission) return true; - - // Verificar si es owner - const isOwner = user.id === resourceId; - return isOwner; - } -} -``` - -### Uso en Controllers - -```typescript -// users.controller.ts -@Controller('api/v1/users') -@UseGuards(JwtAuthGuard, TenantGuard, RbacGuard) -export class UsersController { - - @Get() - @Permissions('users:read') - findAll() { - // Solo usuarios con users:read - } - - @Post() - @Permissions('users:create') - create(@Body() dto: CreateUserDto) { - // Solo usuarios con users:create - } - - @Patch(':id') - @OwnerOrPermission('users:update') - update(@Param('id') id: string, @Body() dto: UpdateUserDto) { - // Owner del recurso O usuarios con users:update - } - - @Delete(':id') - @Permissions('users:delete') - remove(@Param('id') id: string) { - // Solo usuarios con users:delete - } - - @Get('export') - @AnyPermission('users:export', 'users:admin') - export() { - // Usuarios con users:export O users:admin - } -} - -// public.controller.ts -@Controller('api/v1/public') -export class PublicController { - - @Get('health') - @Public() - health() { - // Sin autenticacion - } -} -``` - -### Invalidacion de Cache - -```typescript -// Al cambiar roles de usuario -async updateUserRoles(userId: string, roleIds: string[]) { - await this.userRoleRepository.update(userId, roleIds); - - // Invalidar cache de permisos - await this.cacheManager.del(`permissions:${userId}`); - - // Emitir evento para invalidar en otros servicios - this.eventEmitter.emit('user.roles.changed', { userId }); -} - -// Al modificar permisos de un rol -async updateRolePermissions(roleId: string, permissionIds: string[]) { - await this.rolePermissionRepository.update(roleId, permissionIds); - - // Obtener usuarios con este rol - const users = await this.userRoleRepository.findUsersByRole(roleId); - - // Invalidar cache de todos - for (const user of users) { - await this.cacheManager.del(`permissions:${user.id}`); - } -} -``` - ---- - -## Respuestas de Error - -```typescript -// 401 Unauthorized - No autenticado -{ - "statusCode": 401, - "message": "No autenticado", - "error": "Unauthorized" -} - -// 403 Forbidden - Sin permiso -{ - "statusCode": 403, - "message": "No tienes permiso para realizar esta accion", - "error": "Forbidden" -} -// NOTA: No revelar que permiso falta por seguridad - -// Log interno (no expuesto) -{ - "userId": "user-uuid", - "endpoint": "DELETE /api/v1/users/123", - "requiredPermission": "users:delete", - "userPermissions": ["users:read"], - "result": "denied", - "timestamp": "2025-12-05T10:00:00Z" -} -``` - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-AUTH-002 | JWT para autenticacion | -| RF-ROLE-001 | Roles del sistema | -| RF-ROLE-002 | Permisos del sistema | -| RF-ROLE-003 | Asignacion roles-usuarios | -| Redis | Cache de permisos | - ---- - -## Estimacion - -| Tarea | Puntos | -|-------|--------| -| Backend: Decoradores | 2 | -| Backend: RbacGuard | 3 | -| Backend: OwnerGuard | 2 | -| Backend: Cache de permisos | 2 | -| Backend: Invalidacion cache | 2 | -| Backend: Tests | 3 | -| Documentacion | 1 | -| **Total** | **15 SP** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-tenants/INDICE-RF-TENANT.md b/docs/03-requerimientos/RF-tenants/INDICE-RF-TENANT.md deleted file mode 100644 index 276bbd0..0000000 --- a/docs/03-requerimientos/RF-tenants/INDICE-RF-TENANT.md +++ /dev/null @@ -1,271 +0,0 @@ -# Indice de Requerimientos Funcionales - MGN-004 Tenants - -## Resumen del Modulo - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-004 | -| **Nombre** | Tenants (Multi-Tenancy) | -| **Descripcion** | Arquitectura multi-tenant con aislamiento de datos | -| **Total RFs** | 4 | -| **Story Points** | 100 | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Lista de Requerimientos - -| ID | Nombre | Prioridad | SP | Estado | -|----|--------|-----------|-----|--------| -| [RF-TENANT-001](./RF-TENANT-001.md) | Gestion de Tenants | P0 | 29 | Ready | -| [RF-TENANT-002](./RF-TENANT-002.md) | Configuracion de Tenant | P0 | 19 | Ready | -| [RF-TENANT-003](./RF-TENANT-003.md) | Aislamiento de Datos | P0 | 20 | Ready | -| [RF-TENANT-004](./RF-TENANT-004.md) | Subscripciones y Limites | P1 | 32 | Ready | - ---- - -## Diagrama de Arquitectura Multi-Tenant - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Platform Layer │ -├─────────────────────────────────────────────────────────────────┤ -│ Platform Admin UI │ API Gateway │ Auth Service │ -│ (Gestion tenants) │ (Routing) │ (JWT + tenant) │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Application Layer │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ TenantGuard │ │ RbacGuard │ │LimitGuard │ │ -│ │(Contexto) │ │ (Permisos) │ │(Subscripcion)│ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐│ -│ │ TenantAwareServices ││ -│ │ (Todos los servicios heredan contexto de tenant) ││ -│ └─────────────────────────────────────────────────────────────┘│ -│ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Data Layer │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ PostgreSQL Database (Shared) │ -│ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Tenant A │ │ Tenant B │ │ Tenant C │ │ -│ │ tenant_id │ │ tenant_id │ │ tenant_id │ │ -│ │ =uuid-a │ │ =uuid-b │ │ =uuid-c │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -│ │ -│ Row Level Security (RLS) - Aislamiento automatico │ -│ SET app.current_tenant_id = 'tenant-uuid' │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Flujo de Request Multi-Tenant - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Request Flow │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. Request HTTP │ -│ └─> Authorization: Bearer {JWT con tenant_id} │ -│ │ -│ 2. JwtAuthGuard │ -│ └─> Valida token, extrae user + tenant_id │ -│ │ -│ 3. TenantGuard │ -│ └─> Verifica tenant activo │ -│ └─> Inyecta tenant en request │ -│ │ -│ 4. TenantContextMiddleware │ -│ └─> SET app.current_tenant_id = :tenantId │ -│ │ -│ 5. LimitGuard (si aplica) │ -│ └─> Verifica limites de subscripcion │ -│ │ -│ 6. RbacGuard │ -│ └─> Valida permisos del usuario │ -│ │ -│ 7. Controller │ -│ └─> Ejecuta logica de negocio │ -│ │ -│ 8. TenantAwareService │ -│ └─> Automaticamente filtra por tenant │ -│ │ -│ 9. PostgreSQL RLS │ -│ └─> Ultima linea de defensa │ -│ └─> WHERE tenant_id = current_setting('app.current_tenant_id')│ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Estados del Tenant - -``` - ┌──────────────┐ - │ created │ - └──────┬───────┘ - │ - ▼ - ┌──────────────┐ - ┌──────────│ trial │──────────┐ - │ └──────┬───────┘ │ - │ │ │ - │ (upgrade) │ (trial expires) │ (convert) - │ │ │ - │ ▼ │ - │ ┌──────────────┐ │ - │ │trial_expired │ │ - │ └──────────────┘ │ - │ │ - ▼ ▼ - ┌──────────────┐ ┌──────────────┐ - │ active │◄────────────────────│ active │ - └──────┬───────┘ (reactivate) └──────┬───────┘ - │ │ - │ (suspend) │ - │ │ - ▼ │ - ┌──────────────┐ │ - │ suspended │────────────────────────────┘ - └──────┬───────┘ - │ - │ (schedule delete) - ▼ - ┌──────────────────┐ - │ pending_deletion │ - └────────┬─────────┘ - │ - │ (30 days grace) - ▼ - ┌──────────────┐ - │ deleted │ - └──────────────┘ -``` - ---- - -## Planes de Subscripcion - -| Plan | Usuarios | Storage | Modulos | API Calls | Precio | -|------|----------|---------|---------|-----------|--------| -| **Trial** | 5 | 1 GB | Basicos | 1K/mes | Gratis | -| **Starter** | 10 | 5 GB | Basicos | 10K/mes | $29/mes | -| **Professional** | 50 | 25 GB | Standard | 50K/mes | $99/mes | -| **Enterprise** | ∞ | 100 GB | Premium | 500K/mes | $299/mes | -| **Custom** | Config | Config | Config | Config | Cotizacion | - ---- - -## Tablas de Base de Datos - -| Tabla | Descripcion | -|-------|-------------| -| tenants | Tenants registrados | -| tenant_settings | Configuracion de cada tenant (JSONB) | -| subscriptions | Subscripciones activas y pasadas | -| plans | Planes de subscripcion disponibles | -| invoices | Historial de facturacion | -| tenant_modules | Modulos activos por tenant | - ---- - -## Estimacion Total - -| Capa | Story Points | -|------|--------------| -| Backend: CRUD Tenants | 12 | -| Backend: Settings | 7 | -| Backend: RLS & Guards | 15 | -| Backend: Subscriptions | 15 | -| Backend: Billing | 10 | -| Backend: Tests | 12 | -| Frontend: Platform Admin | 15 | -| Frontend: Tenant Settings | 8 | -| Frontend: Subscription UI | 8 | -| Frontend: Tests | 6 | -| **Total** | **108 SP** | - -> Nota: Los 100 SP indicados en resumen corresponden a la suma de RF individuales. - ---- - -## Definition of Done del Modulo - -- [ ] RF-TENANT-001: CRUD de tenants funcional -- [ ] RF-TENANT-002: Settings por tenant operativos -- [ ] RF-TENANT-003: RLS aplicado en TODAS las tablas -- [ ] RF-TENANT-003: Tests de aislamiento pasando -- [ ] RF-TENANT-004: Planes y limites configurados -- [ ] RF-TENANT-004: Enforcement de limites activo -- [ ] Platform Admin UI completa -- [ ] Tests unitarios > 80% coverage -- [ ] Tests de aislamiento exhaustivos -- [ ] Security review de RLS aprobado -- [ ] Performance tests con multiples tenants - ---- - -## Consideraciones de Seguridad - -### Aislamiento de Datos - -- RLS como ultima linea de defensa -- Validacion en TODAS las capas -- Nunca confiar en input del cliente -- Auditar accesos cross-tenant - -### Platform Admin - -- Autenticacion separada -- 2FA obligatorio -- Logs de todas las acciones -- Switch de tenant explicito y auditado - -### Datos Sensibles - -- Encriptar datos sensibles por tenant -- Claves de encriptacion por tenant -- Backups separados (opcional enterprise) - ---- - -## Notas de Implementacion - -### Orden Recomendado - -1. **Primero**: RF-TENANT-003 (RLS) - Base para todo lo demas -2. **Segundo**: RF-TENANT-001 (CRUD) - Crear tenants -3. **Tercero**: RF-TENANT-002 (Settings) - Configurar tenants -4. **Cuarto**: RF-TENANT-004 (Subscriptions) - Monetizacion - -### Migracion de Datos Existentes - -Si hay datos sin tenant_id: -1. Crear tenant "default" -2. Asignar todos los datos al default -3. Habilitar RLS -4. Migrar datos a tenants especificos - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 RFs | diff --git a/docs/03-requerimientos/RF-tenants/RF-TENANT-001.md b/docs/03-requerimientos/RF-tenants/RF-TENANT-001.md deleted file mode 100644 index 5a3278d..0000000 --- a/docs/03-requerimientos/RF-tenants/RF-TENANT-001.md +++ /dev/null @@ -1,396 +0,0 @@ -# RF-TENANT-001: Gestion de Tenants - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-TENANT-001 | -| **Modulo** | MGN-004 Tenants | -| **Prioridad** | P0 - Critica | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe soportar una arquitectura multi-tenant donde cada organizacion (tenant) tiene su propio espacio aislado de datos. Los super administradores pueden crear, gestionar y eliminar tenants. Cada tenant tiene su propia configuracion, usuarios, roles y datos de negocio completamente aislados. - ---- - -## Actores - -| Actor | Descripcion | -|-------|-------------| -| Platform Admin | Administrador de la plataforma, gestiona todos los tenants | -| Tenant Admin | Administrador de un tenant especifico | -| Sistema | Procesos automaticos de gestion de tenants | - ---- - -## Precondiciones - -1. Platform Admin autenticado con permisos de plataforma -2. Sistema de base de datos configurado con soporte para schemas/RLS - ---- - -## Flujo Principal - -### Crear Tenant - -``` -1. Platform Admin accede a Panel de Plataforma > Tenants -2. Click en "Nuevo Tenant" -3. Sistema muestra formulario: - - Nombre de la organizacion - - Slug (URL-friendly identifier) - - Email del admin inicial - - Plan de subscripcion - - Modulos a activar -4. Admin completa datos -5. Click en "Crear Tenant" -6. Sistema valida unicidad del slug -7. Sistema crea el tenant en base de datos -8. Sistema crea usuario admin inicial -9. Sistema asigna roles built-in al tenant -10. Sistema configura RLS policies -11. Sistema envia email de bienvenida al admin -12. Sistema muestra confirmacion -``` - -### Listar Tenants - -``` -1. Platform Admin accede a Panel de Plataforma > Tenants -2. Sistema muestra tabla con: - - Nombre del tenant - - Slug - - Plan de subscripcion - - Estado (active, suspended, trial) - - Usuarios activos - - Fecha de creacion - - Ultimo acceso -3. Admin puede filtrar por estado, plan -4. Admin puede buscar por nombre o slug -``` - -### Ver Detalle de Tenant - -``` -1. Platform Admin click en un tenant -2. Sistema muestra: - - Informacion basica - - Estadisticas de uso - - Lista de usuarios - - Modulos activos - - Historial de facturacion - - Logs de actividad -``` - -### Suspender Tenant - -``` -1. Platform Admin click en "Suspender" de un tenant -2. Sistema muestra dialogo de confirmacion -3. Admin selecciona razon de suspension -4. Admin confirma -5. Sistema cambia estado a "suspended" -6. Sistema revoca todos los tokens activos -7. Sistema envia notificacion al admin del tenant -8. Usuarios del tenant no pueden acceder -``` - -### Eliminar Tenant - -``` -1. Platform Admin click en "Eliminar" de un tenant -2. Sistema muestra advertencia severa -3. Admin debe escribir el slug del tenant para confirmar -4. Sistema programa eliminacion (grace period 30 dias) -5. Sistema notifica al admin del tenant -6. Despues del grace period, sistema elimina datos -``` - ---- - -## Flujos Alternativos - -### FA1: Slug duplicado - -``` -1. En paso 6 del flujo crear -2. Sistema detecta slug ya existe -3. Sistema sugiere alternativas disponibles -4. Admin selecciona o modifica -5. Continua desde paso 5 -``` - -### FA2: Reactivar tenant suspendido - -``` -1. Platform Admin accede a tenant suspendido -2. Click en "Reactivar" -3. Sistema verifica estado de cuenta -4. Si hay problemas de pago, muestra opcion de resolver -5. Sistema cambia estado a "active" -6. Usuarios pueden acceder nuevamente -``` - -### FA3: Cancelar eliminacion programada - -``` -1. Dentro del grace period -2. Platform Admin accede al tenant marcado para eliminacion -3. Click en "Cancelar eliminacion" -4. Sistema restaura estado anterior -5. Notifica al admin del tenant -``` - ---- - -## Reglas de Negocio - -| ID | Regla | -|----|-------| -| RN-001 | Slug de tenant unico globalmente | -| RN-002 | Slug: 3-50 caracteres, lowercase, alfanumerico con guiones | -| RN-003 | Cada tenant debe tener al menos un admin | -| RN-004 | Suspension inmediata, eliminacion con grace period | -| RN-005 | Grace period de eliminacion: 30 dias | -| RN-006 | Datos de tenant eliminado son irrecuperables | -| RN-007 | Un tenant puede tener maximo segun plan (ej: 100 usuarios en plan basico) | -| RN-008 | Tenant trial expira en 14 dias | - ---- - -## Estados del Tenant - -``` - ┌─────────┐ - │ trial │ - └────┬────┘ - │ (subscription) - ▼ - (reactivate) ┌─────────┐ (suspend) - ┌─────────│ active │─────────┐ - │ └────┬────┘ │ - │ │ ▼ - │ │ ┌───────────┐ - │ │ │ suspended │ - │ │ └─────┬─────┘ - │ │ │ - └──────────────┼──────────────┘ - │ (delete) - ▼ - ┌─────────────────┐ - │ pending_deletion│ - └────────┬────────┘ - │ (30 days) - ▼ - ┌──────────┐ - │ deleted │ - └──────────┘ -``` - ---- - -## Criterios de Aceptacion - -### Escenario 1: Crear tenant exitosamente - -```gherkin -Given un Platform Admin autenticado -When crea un tenant con: - | name | Empresa ABC | - | slug | empresa-abc | - | email | admin@empresaabc.com | - | plan | professional | -Then el sistema crea el tenant - And crea usuario admin con email proporcionado - And asigna roles built-in al tenant - And envia email de bienvenida - And responde con status 201 -``` - -### Escenario 2: Listar tenants con filtros - -```gherkin -Given 50 tenants en la plataforma -When Platform Admin filtra por status="active" y plan="professional" -Then el sistema retorna solo tenants activos con plan professional - And incluye metricas de cada tenant -``` - -### Escenario 3: No crear tenant con slug duplicado - -```gherkin -Given un tenant existente con slug "empresa-abc" -When se intenta crear otro con slug "empresa-abc" -Then el sistema responde con status 409 - And sugiere slugs alternativos -``` - -### Escenario 4: Suspender tenant - -```gherkin -Given un tenant activo "empresa-abc" con 10 usuarios -When Platform Admin lo suspende por "falta_pago" -Then el estado cambia a "suspended" - And los 10 usuarios no pueden acceder - And se envia email al admin del tenant -``` - -### Escenario 5: Grace period de eliminacion - -```gherkin -Given un tenant marcado para eliminacion hace 25 dias -When han pasado 30 dias desde la marca -Then el sistema elimina permanentemente los datos - And el slug queda disponible para reusar -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Platform Admin - Tenants [+ Nuevo Tenant] | -+------------------------------------------------------------------+ -| Buscar: [___________________] [Estado: Todos ▼] [Plan: Todos ▼] | -+------------------------------------------------------------------+ -| | Nombre | Slug | Plan | Estado | Usuarios | ⚙ | -|---|----------------|-------------|--------|--------|----------|-----| -| | Empresa ABC | empresa-abc | Pro | Active | 45 | 👁✏🗑| -| | Comercial XYZ | comercial | Basic | Active | 12 | 👁✏🗑| -| | Demo Company | demo-co | Trial | Trial | 3 | 👁✏🗑| -| | Old Client | old-client | Basic | Susp. | 0 | 👁✏⚠ | -+------------------------------------------------------------------+ -| Mostrando 1-4 de 50 [< Anterior] [Siguiente >]| -+------------------------------------------------------------------+ - -Modal: Crear Tenant -┌──────────────────────────────────────────────────────────────────┐ -│ NUEVO TENANT │ -├──────────────────────────────────────────────────────────────────┤ -│ Nombre* [_______________________________] │ -│ Slug* [_______________________________] │ -│ URL: https://erp.com/empresa-abc │ -│ │ -│ ADMIN INICIAL │ -│ Email* [_______________________________] │ -│ Nombre [_______________________________] │ -│ │ -│ SUBSCRIPCION │ -│ Plan [Professional ▼] │ -│ Periodo trial [14 dias ▼] │ -│ │ -│ MODULOS │ -│ ☑ Auth ☑ Users ☑ Roles │ -│ ☑ Inventory ☑ Financial ☐ CRM │ -│ ☑ Reports ☐ Advanced ☐ API │ -│ │ -│ [ Cancelar ] [ Crear Tenant ] │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Crear tenant (Platform Admin only) -POST /api/v1/platform/tenants -{ - "name": "Empresa ABC", - "slug": "empresa-abc", - "adminEmail": "admin@empresaabc.com", - "adminName": "Juan Perez", - "planId": "plan-professional", - "moduleIds": ["mod-auth", "mod-users", "mod-inventory"], - "trialDays": 14 -} - -// Response 201 -{ - "id": "tenant-uuid", - "name": "Empresa ABC", - "slug": "empresa-abc", - "status": "trial", - "plan": { "id": "...", "name": "Professional" }, - "admin": { "id": "...", "email": "admin@empresaabc.com" }, - "trialEndsAt": "2025-12-19T00:00:00Z", - "createdAt": "2025-12-05T10:00:00Z" -} - -// Listar tenants -GET /api/v1/platform/tenants?status=active&plan=professional&page=1&limit=20 - -// Ver detalle -GET /api/v1/platform/tenants/:id - -// Actualizar tenant -PATCH /api/v1/platform/tenants/:id - -// Suspender tenant -POST /api/v1/platform/tenants/:id/suspend -{ "reason": "payment_failed", "notes": "Invoice #123 unpaid" } - -// Reactivar tenant -POST /api/v1/platform/tenants/:id/reactivate - -// Programar eliminacion -POST /api/v1/platform/tenants/:id/schedule-deletion - -// Cancelar eliminacion -POST /api/v1/platform/tenants/:id/cancel-deletion - -// Eliminar inmediatamente (solo desarrollo) -DELETE /api/v1/platform/tenants/:id?force=true -``` - -### Validaciones - -| Campo | Regla | -|-------|-------| -| name | Required, 3-100 chars | -| slug | Required, 3-50 chars, lowercase, unique | -| adminEmail | Required, valid email, unique | -| planId | Required, plan existente | - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-AUTH-001 | Para crear usuario admin | -| RF-ROLE-001 | Para crear roles built-in | -| Database | Soporte para RLS o schemas separados | - ---- - -## Estimacion - -| Tarea | Puntos | -|-------|--------| -| Backend: CRUD endpoints | 5 | -| Backend: Lifecycle (suspend/delete) | 4 | -| Backend: Tenant provisioning | 5 | -| Backend: Tests | 3 | -| Frontend: TenantsListPage | 4 | -| Frontend: TenantForm | 3 | -| Frontend: TenantDetail | 3 | -| Frontend: Tests | 2 | -| **Total** | **29 SP** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-tenants/RF-TENANT-002.md b/docs/03-requerimientos/RF-tenants/RF-TENANT-002.md deleted file mode 100644 index 111e919..0000000 --- a/docs/03-requerimientos/RF-tenants/RF-TENANT-002.md +++ /dev/null @@ -1,370 +0,0 @@ -# RF-TENANT-002: Configuracion de Tenant - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-TENANT-002 | -| **Modulo** | MGN-004 Tenants | -| **Prioridad** | P0 - Critica | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a los administradores de cada tenant personalizar la configuracion de su organizacion, incluyendo informacion de la empresa, branding, configuraciones regionales, y parametros operativos. Esta configuracion afecta el comportamiento del sistema para todos los usuarios del tenant. - ---- - -## Actores - -| Actor | Descripcion | -|-------|-------------| -| Tenant Admin | Administrador del tenant que configura | -| Platform Admin | Puede modificar configuracion de cualquier tenant | - ---- - -## Precondiciones - -1. Usuario autenticado con permiso `tenants:update` o `settings:update` -2. Tenant activo - ---- - -## Categorias de Configuracion - -### 1. Informacion de la Empresa - -| Campo | Descripcion | Ejemplo | -|-------|-------------|---------| -| companyName | Nombre legal | Empresa ABC S.A. de C.V. | -| tradeName | Nombre comercial | ABC Corp | -| taxId | RFC/NIT/RUC | EABC850101ABC | -| address | Direccion fiscal | Av. Principal #123 | -| city | Ciudad | Ciudad de Mexico | -| state | Estado/Provincia | CDMX | -| country | Pais | MX | -| postalCode | Codigo postal | 06600 | -| phone | Telefono principal | +52 55 1234 5678 | -| email | Email de contacto | contacto@empresaabc.com | -| website | Sitio web | https://empresaabc.com | - -### 2. Branding - -| Campo | Descripcion | Ejemplo | -|-------|-------------|---------| -| logo | Logo principal | URL/base64 | -| logoSmall | Logo pequeno/icono | URL/base64 | -| favicon | Favicon | URL/base64 | -| primaryColor | Color primario | #3B82F6 | -| secondaryColor | Color secundario | #10B981 | -| accentColor | Color de acento | #F59E0B | - -### 3. Configuracion Regional - -| Campo | Descripcion | Ejemplo | -|-------|-------------|---------| -| defaultLanguage | Idioma por defecto | es | -| defaultTimezone | Zona horaria | America/Mexico_City | -| defaultCurrency | Moneda por defecto | MXN | -| dateFormat | Formato de fecha | DD/MM/YYYY | -| timeFormat | Formato de hora | 24h | -| numberFormat | Formato numerico | es-MX | -| firstDayOfWeek | Primer dia semana | 1 (Lunes) | - -### 4. Configuracion Operativa - -| Campo | Descripcion | Ejemplo | -|-------|-------------|---------| -| fiscalYearStart | Inicio ano fiscal | 01-01 | -| workingDays | Dias laborables | [1,2,3,4,5] | -| businessHoursStart | Inicio horario | 09:00 | -| businessHoursEnd | Fin horario | 18:00 | -| defaultTaxRate | Tasa impuesto default | 16 | -| invoicePrefix | Prefijo facturas | FAC- | -| invoiceNextNumber | Siguiente numero | 1001 | - -### 5. Configuracion de Seguridad - -| Campo | Descripcion | Ejemplo | -|-------|-------------|---------| -| passwordMinLength | Longitud minima pass | 8 | -| passwordRequireSpecial | Requerir especiales | true | -| sessionTimeout | Timeout sesion (min) | 30 | -| maxLoginAttempts | Intentos maximos | 5 | -| lockoutDuration | Duracion bloqueo (min) | 15 | -| mfaRequired | MFA obligatorio | false | -| ipWhitelist | IPs permitidas | [] | - ---- - -## Flujo Principal - -### Ver Configuracion - -``` -1. Tenant Admin accede a Configuracion > Empresa -2. Sistema muestra configuracion actual organizada en tabs: - - Informacion General - - Branding - - Regional - - Operaciones - - Seguridad -3. Admin puede navegar entre tabs -``` - -### Actualizar Configuracion - -``` -1. Admin modifica campos deseados -2. Sistema valida en tiempo real -3. Admin click en "Guardar Cambios" -4. Sistema valida todos los campos -5. Sistema guarda configuracion -6. Sistema aplica cambios (algunos requieren recarga) -7. Sistema muestra confirmacion -``` - -### Subir Logo - -``` -1. Admin click en "Cambiar Logo" -2. Sistema muestra modal de upload -3. Admin selecciona imagen -4. Sistema valida formato y tamano -5. Sistema genera versiones (original, thumbnail) -6. Sistema actualiza logo -7. Cambio visible inmediatamente en UI -``` - ---- - -## Reglas de Negocio - -| ID | Regla | -|----|-------| -| RN-001 | Configuracion se hereda de defaults de plataforma | -| RN-002 | Tenant puede sobrescribir cualquier default | -| RN-003 | Algunos campos requieren validacion especial (taxId por pais) | -| RN-004 | Logo: max 5MB, formatos JPG/PNG/SVG | -| RN-005 | Colores deben ser hex validos | -| RN-006 | Cambios de seguridad aplican en siguiente login | -| RN-007 | IP Whitelist vacia = sin restriccion | - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver configuracion actual - -```gherkin -Given un Tenant Admin autenticado -When accede a GET /api/v1/tenant/settings -Then el sistema retorna configuracion completa - And incluye valores del tenant - And incluye defaults de plataforma donde no hay override -``` - -### Escenario 2: Actualizar informacion de empresa - -```gherkin -Given un Tenant Admin autenticado -When actualiza companyName a "Nueva Empresa S.A." -Then el sistema guarda el cambio - And retorna configuracion actualizada - And el nombre aparece en toda la UI del tenant -``` - -### Escenario 3: Personalizar branding - -```gherkin -Given un Tenant Admin con configuracion default -When sube un logo y cambia primaryColor a "#FF5733" -Then el sistema guarda el logo en storage - And actualiza el color primario - And la UI refleja los cambios de branding -``` - -### Escenario 4: Configuracion de seguridad - -```gherkin -Given configuracion de seguridad actual -When Tenant Admin establece passwordMinLength=12 -Then el sistema guarda la configuracion - And nuevas contrasenas deben tener minimo 12 caracteres - And usuarios existentes no son afectados hasta cambio de pass -``` - -### Escenario 5: Herencia de defaults - -```gherkin -Given tenant sin configuracion de timezone - And plataforma con default "America/Mexico_City" -When se consulta configuracion del tenant -Then timezone muestra "America/Mexico_City" - And se indica que es valor heredado (no override) -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Configuracion de Empresa | -+------------------------------------------------------------------+ -| [General] [Branding] [Regional] [Operaciones] [Seguridad] | -+------------------------------------------------------------------+ -| | -| Tab: General | -| ┌─────────────────────────────────────────────────────────────┐ | -| │ INFORMACION LEGAL │ | -| │ │ | -| │ Razon Social [Empresa ABC S.A. de C.V. ] │ | -| │ Nombre Comercial [ABC Corp ] │ | -| │ RFC [EABC850101ABC ] │ | -| │ │ | -| │ DIRECCION FISCAL │ | -| │ │ | -| │ Direccion [Av. Principal #123 ] │ | -| │ Ciudad [Ciudad de Mexico ] │ | -| │ Estado [CDMX ▼] │ | -| │ Codigo Postal [06600 ] │ | -| │ Pais [Mexico ▼] │ | -| │ │ | -| │ CONTACTO │ | -| │ │ | -| │ Telefono [+52 55 1234 5678 ] │ | -| │ Email [contacto@empresaabc.com ] │ | -| │ Sitio Web [https://empresaabc.com ] │ | -| └─────────────────────────────────────────────────────────────┘ | -| | -| [ Cancelar ] [ Guardar Cambios ] | -+------------------------------------------------------------------+ - -Tab: Branding -┌─────────────────────────────────────────────────────────────────┐ -│ LOGOS │ -│ │ -│ +-------------+ +-------+ │ -│ | | | | │ -│ | LOGO | | ICON | [Cambiar Logo] [Cambiar Icono] │ -│ | | | | │ -│ +-------------+ +-------+ │ -│ │ -│ COLORES │ -│ │ -│ Primario [#3B82F6] ████████ │ -│ Secundario [#10B981] ████████ │ -│ Acento [#F59E0B] ████████ │ -│ │ -│ [Vista previa] │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Obtener configuracion completa -GET /api/v1/tenant/settings - -// Response 200 -{ - "company": { - "companyName": "Empresa ABC S.A. de C.V.", - "tradeName": "ABC Corp", - "taxId": "EABC850101ABC", - // ... - "_inherited": false - }, - "branding": { - "logo": "https://storage.../logo.png", - "primaryColor": "#3B82F6", - // ... - "_inherited": ["secondaryColor"] // campos heredados - }, - "regional": { - "defaultLanguage": "es", - "defaultTimezone": "America/Mexico_City", - // ... - }, - "operational": { ... }, - "security": { ... } -} - -// Actualizar configuracion (parcial) -PATCH /api/v1/tenant/settings -{ - "company": { - "companyName": "Nueva Empresa S.A." - }, - "branding": { - "primaryColor": "#FF5733" - } -} - -// Subir logo -POST /api/v1/tenant/settings/logo -Content-Type: multipart/form-data -logo: [file] -type: "main" | "small" | "favicon" - -// Reset a defaults -POST /api/v1/tenant/settings/reset -{ "sections": ["branding"] } // o "all" -``` - -### Estructura de Settings - -```typescript -interface TenantSettings { - company: CompanySettings; - branding: BrandingSettings; - regional: RegionalSettings; - operational: OperationalSettings; - security: SecuritySettings; -} - -// Se guarda como JSONB en la tabla tenant_settings -// con merge inteligente con defaults -``` - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-TENANT-001 | Tenant debe existir | -| Storage | Para logos e imagenes | -| Cache | Para settings frecuentes | - ---- - -## Estimacion - -| Tarea | Puntos | -|-------|--------| -| Backend: Settings endpoints | 3 | -| Backend: Logo upload | 2 | -| Backend: Merge con defaults | 2 | -| Backend: Tests | 2 | -| Frontend: SettingsPage (5 tabs) | 6 | -| Frontend: BrandingPreview | 2 | -| Frontend: Tests | 2 | -| **Total** | **19 SP** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-tenants/RF-TENANT-003.md b/docs/03-requerimientos/RF-tenants/RF-TENANT-003.md deleted file mode 100644 index 1caef79..0000000 --- a/docs/03-requerimientos/RF-tenants/RF-TENANT-003.md +++ /dev/null @@ -1,424 +0,0 @@ -# RF-TENANT-003: Aislamiento de Datos - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-TENANT-003 | -| **Modulo** | MGN-004 Tenants | -| **Prioridad** | P0 - Critica | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe garantizar el aislamiento completo de datos entre tenants. Ningun usuario de un tenant debe poder acceder, ver o modificar datos de otro tenant. Este aislamiento se implementa mediante Row Level Security (RLS) en PostgreSQL, con validacion adicional en la capa de aplicacion. - ---- - -## Actores - -| Actor | Descripcion | -|-------|-------------| -| Sistema | Aplica automaticamente el filtro de tenant | -| Usuario | Cualquier usuario autenticado | -| Platform Admin | Puede acceder a multiples tenants (con switch explicito) | - ---- - -## Estrategia de Aislamiento - -### Arquitectura: Shared Database + RLS - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ PostgreSQL Database │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │ -│ │ Tenant A │ │ Tenant B │ │ Tenant C │ │ -│ │ (tenant_id=1) │ │ (tenant_id=2) │ │ (tenant_id=3)│ │ -│ │ │ │ │ │ │ │ -│ │ users: 50 │ │ users: 30 │ │ users: 100 │ │ -│ │ products: 1000 │ │ products: 500 │ │ products: 2k │ │ -│ │ orders: 5000 │ │ orders: 2000 │ │ orders: 10k │ │ -│ └──────────────────┘ └──────────────────┘ └───────────────┘ │ -│ │ -│ RLS Policies: Todas las tablas filtran por tenant_id │ -│ app.current_tenant_id = variable de sesion │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Capas de Proteccion - -``` -Request HTTP - │ - ▼ -┌─────────────────┐ -│ 1. JwtAuthGuard │ Extrae tenant_id del token -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 2. TenantGuard │ Valida tenant activo, setea contexto -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 3. Middleware │ SET app.current_tenant_id = :tenantId -│ PostgreSQL │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 4. RLS Policy │ WHERE tenant_id = current_setting('app.current_tenant_id') -│ PostgreSQL │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ 5. Application │ Validacion adicional en queries -│ Layer │ -└─────────────────┘ -``` - ---- - -## Implementacion RLS - -### Configuracion Base - -```sql --- Habilitar RLS en todas las tablas con tenant_id -ALTER TABLE core_users.users ENABLE ROW LEVEL SECURITY; -ALTER TABLE core_rbac.roles ENABLE ROW LEVEL SECURITY; -ALTER TABLE core_inventory.products ENABLE ROW LEVEL SECURITY; --- ... todas las tablas de negocio - --- Funcion para obtener tenant actual -CREATE OR REPLACE FUNCTION current_tenant_id() -RETURNS UUID AS $$ -BEGIN - RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID; -END; -$$ LANGUAGE plpgsql STABLE; -``` - -### Politicas de Lectura - -```sql --- Usuarios solo ven datos de su tenant -CREATE POLICY tenant_isolation_select ON core_users.users - FOR SELECT - USING (tenant_id = current_tenant_id()); - --- Platform Admin puede ver todos (con contexto especial) -CREATE POLICY platform_admin_select ON core_users.users - FOR SELECT - USING ( - tenant_id = current_tenant_id() - OR current_setting('app.is_platform_admin', true) = 'true' - ); -``` - -### Politicas de Escritura - -```sql --- Insert: debe ser del tenant actual -CREATE POLICY tenant_isolation_insert ON core_users.users - FOR INSERT - WITH CHECK (tenant_id = current_tenant_id()); - --- Update: solo registros del tenant actual -CREATE POLICY tenant_isolation_update ON core_users.users - FOR UPDATE - USING (tenant_id = current_tenant_id()) - WITH CHECK (tenant_id = current_tenant_id()); - --- Delete: solo registros del tenant actual -CREATE POLICY tenant_isolation_delete ON core_users.users - FOR DELETE - USING (tenant_id = current_tenant_id()); -``` - ---- - -## Reglas de Negocio - -| ID | Regla | -|----|-------| -| RN-001 | Toda tabla de datos de negocio DEBE tener columna tenant_id | -| RN-002 | tenant_id es NOT NULL y tiene FK a tenants | -| RN-003 | RLS habilitado en TODAS las tablas con tenant_id | -| RN-004 | Queries sin contexto de tenant fallan (no retornan datos) | -| RN-005 | Platform Admin debe hacer switch explicito de tenant | -| RN-006 | Logs de auditoria registran tenant_id | -| RN-007 | Backups se pueden hacer por tenant individual | -| RN-008 | Indices deben incluir tenant_id para performance | - ---- - -## Criterios de Aceptacion - -### Escenario 1: Usuario solo ve datos de su tenant - -```gherkin -Given usuario de Tenant A autenticado - And 100 productos en Tenant A - And 50 productos en Tenant B -When consulta GET /api/v1/products -Then solo ve los 100 productos de Tenant A - And no ve ningun producto de Tenant B -``` - -### Escenario 2: Usuario no puede acceder a recurso de otro tenant - -```gherkin -Given usuario de Tenant A autenticado - And producto "P-001" pertenece a Tenant B -When intenta GET /api/v1/products/P-001 -Then el sistema responde con status 404 - And el mensaje es "Recurso no encontrado" - And NO revela que el recurso existe en otro tenant -``` - -### Escenario 3: Crear recurso asigna tenant automaticamente - -```gherkin -Given usuario de Tenant A autenticado -When crea un producto sin especificar tenant_id -Then el sistema asigna automaticamente tenant_id de Tenant A - And el producto queda en Tenant A -``` - -### Escenario 4: No permitir modificar tenant_id - -```gherkin -Given usuario de Tenant A autenticado - And producto existente en Tenant A -When intenta actualizar tenant_id a Tenant B -Then el sistema responde con status 400 - And el mensaje es "No se puede cambiar el tenant de un recurso" -``` - -### Escenario 5: Platform Admin switch de tenant - -```gherkin -Given Platform Admin autenticado -When hace POST /api/v1/platform/switch-tenant/tenant-b-id -Then el contexto cambia a Tenant B - And puede ver datos de Tenant B - And no ve datos de Tenant A -``` - -### Escenario 6: Query sin contexto de tenant - -```gherkin -Given conexion a base de datos sin app.current_tenant_id -When se ejecuta SELECT * FROM users -Then el resultado es vacio - And no se produce error - And RLS previene acceso a datos -``` - ---- - -## Notas Tecnicas - -### TenantGuard Implementation - -```typescript -// guards/tenant.guard.ts -@Injectable() -export class TenantGuard implements CanActivate { - constructor( - private dataSource: DataSource, - private tenantService: TenantService, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const user = request.user; - - if (!user?.tenantId) { - throw new UnauthorizedException('Tenant no identificado'); - } - - // Verificar tenant activo - const tenant = await this.tenantService.findOne(user.tenantId); - if (!tenant || tenant.status !== 'active') { - throw new ForbiddenException('Tenant no disponible'); - } - - // Setear contexto de tenant en request - request.tenant = tenant; - request.tenantId = tenant.id; - - return true; - } -} -``` - -### TenantContext Middleware - -```typescript -// middleware/tenant-context.middleware.ts -@Injectable() -export class TenantContextMiddleware implements NestMiddleware { - constructor(private dataSource: DataSource) {} - - async use(req: Request, res: Response, next: NextFunction) { - const tenantId = req['tenantId']; - - if (tenantId) { - // Setear variable de sesion PostgreSQL para RLS - await this.dataSource.query( - `SET LOCAL app.current_tenant_id = '${tenantId}'` - ); - } - - next(); - } -} -``` - -### Base Entity con Tenant - -```typescript -// entities/tenant-base.entity.ts -export abstract class TenantBaseEntity { - @Column({ name: 'tenant_id' }) - tenantId: string; - - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; - - @BeforeInsert() - setTenantId() { - // Se setea desde el contexto en el servicio - // No permitir override manual - } -} - -// Uso en entidades -@Entity({ schema: 'core_inventory', name: 'products' }) -export class Product extends TenantBaseEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - name: string; - // ... -} -``` - -### Base Service con Tenant - -```typescript -// services/tenant-aware.service.ts -export abstract class TenantAwareService { - constructor( - protected repository: Repository, - @Inject(REQUEST) protected request: Request, - ) {} - - protected get tenantId(): string { - return this.request['tenantId']; - } - - async findAll(options?: FindManyOptions): Promise { - return this.repository.find({ - ...options, - where: { - ...options?.where, - tenantId: this.tenantId, - } as any, - }); - } - - async findOne(id: string): Promise { - return this.repository.findOne({ - where: { - id, - tenantId: this.tenantId, - } as any, - }); - } - - async create(dto: DeepPartial): Promise { - const entity = this.repository.create({ - ...dto, - tenantId: this.tenantId, - } as any); - return this.repository.save(entity); - } -} -``` - ---- - -## Consideraciones de Performance - -### Indices Recomendados - -```sql --- Indice compuesto para queries filtradas por tenant -CREATE INDEX idx_users_tenant_email ON core_users.users(tenant_id, email); -CREATE INDEX idx_products_tenant_sku ON core_inventory.products(tenant_id, sku); -CREATE INDEX idx_orders_tenant_date ON core_sales.orders(tenant_id, created_at DESC); - --- Indice parcial para tenants activos -CREATE INDEX idx_users_active_tenant ON core_users.users(tenant_id) - WHERE deleted_at IS NULL; -``` - -### Query Optimization - -```sql --- BUENO: Usa el indice compuesto -EXPLAIN ANALYZE -SELECT * FROM users -WHERE tenant_id = 'tenant-uuid' AND email = 'user@example.com'; - --- MALO: Full table scan, luego filtro -EXPLAIN ANALYZE -SELECT * FROM users -WHERE email = 'user@example.com'; -- Sin tenant en WHERE, RLS agrega despues -``` - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| PostgreSQL 12+ | Soporte completo de RLS | -| RF-TENANT-001 | Tenants existentes | -| RF-AUTH-002 | JWT con tenant_id | - ---- - -## Estimacion - -| Tarea | Puntos | -|-------|--------| -| Database: RLS policies | 5 | -| Database: Indices | 2 | -| Backend: TenantGuard | 3 | -| Backend: TenantContextMiddleware | 2 | -| Backend: TenantAwareService base | 3 | -| Backend: Tests de aislamiento | 5 | -| **Total** | **20 SP** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-tenants/RF-TENANT-004.md b/docs/03-requerimientos/RF-tenants/RF-TENANT-004.md deleted file mode 100644 index b22ebde..0000000 --- a/docs/03-requerimientos/RF-tenants/RF-TENANT-004.md +++ /dev/null @@ -1,460 +0,0 @@ -# RF-TENANT-004: Subscripciones y Limites - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-TENANT-004 | -| **Modulo** | MGN-004 Tenants | -| **Prioridad** | P1 - Alta | -| **Estado** | Ready | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe gestionar planes de subscripcion para los tenants, controlando los limites de uso (usuarios, storage, modulos) y el ciclo de facturacion. Cada plan define que funcionalidades y recursos estan disponibles para el tenant. - ---- - -## Actores - -| Actor | Descripcion | -|-------|-------------| -| Platform Admin | Gestiona planes y subscripciones | -| Tenant Admin | Ve su subscripcion y puede solicitar upgrades | -| Sistema | Aplica limites automaticamente | -| Billing System | Procesa pagos y renovaciones | - ---- - -## Planes de Subscripcion - -### Planes Disponibles - -| Plan | Usuarios | Storage | Modulos | Precio/mes | -|------|----------|---------|---------|------------| -| Trial | 5 | 1 GB | Basicos | Gratis (14 dias) | -| Starter | 10 | 5 GB | Basicos | $29 USD | -| Professional | 50 | 25 GB | Todos Standard | $99 USD | -| Enterprise | Ilimitado | 100 GB | Todos + Premium | $299 USD | -| Custom | Configurable | Configurable | Configurable | Cotizacion | - -### Modulos por Plan - -| Modulo | Trial | Starter | Professional | Enterprise | -|--------|-------|---------|--------------|------------| -| Auth | ✓ | ✓ | ✓ | ✓ | -| Users | ✓ | ✓ | ✓ | ✓ | -| Roles | ✓ | ✓ | ✓ | ✓ | -| Inventory | ✓ | ✓ | ✓ | ✓ | -| Financial | - | ✓ | ✓ | ✓ | -| Reports | - | Basic | Full | Full | -| CRM | - | - | ✓ | ✓ | -| Advanced Analytics | - | - | - | ✓ | -| API Access | - | - | ✓ | ✓ | -| White Label | - | - | - | ✓ | -| Priority Support | - | - | - | ✓ | - ---- - -## Flujo Principal - -### Ver Subscripcion Actual - -``` -1. Tenant Admin accede a Configuracion > Subscripcion -2. Sistema muestra: - - Plan actual - - Fecha de inicio - - Proxima renovacion - - Uso actual vs limites - - Historial de facturacion -3. Admin puede ver planes disponibles para upgrade -``` - -### Solicitar Upgrade - -``` -1. Tenant Admin click en "Cambiar Plan" -2. Sistema muestra comparativa de planes -3. Admin selecciona nuevo plan -4. Sistema calcula costo prorrateado -5. Admin confirma y procede al pago -6. Sistema procesa pago -7. Sistema actualiza subscripcion -8. Nuevos limites aplican inmediatamente -9. Sistema envia confirmacion por email -``` - -### Renovacion Automatica - -``` -1. Sistema detecta subscripcion por vencer (3 dias) -2. Sistema envia recordatorio de renovacion -3. En fecha de renovacion: - a. Sistema intenta cobrar metodo de pago guardado - b. Si exitoso: renueva subscripcion - c. Si falla: marca como "payment_pending" -4. Si pago pendiente por 7 dias: - a. Sistema envia advertencias - b. Sistema suspende tenant -``` - -### Cancelar Subscripcion - -``` -1. Tenant Admin solicita cancelacion -2. Sistema muestra encuesta de salida -3. Admin confirma cancelacion -4. Sistema programa cancelacion para fin de periodo -5. Tenant sigue activo hasta fin de periodo pagado -6. Al terminar periodo: downgrade a Trial o suspension -``` - ---- - -## Limites y Enforcement - -### Tipos de Limites - -| Tipo | Comportamiento | -|------|----------------| -| Hard Limit | Bloquea la accion inmediatamente | -| Soft Limit | Permite con advertencia, bloquea al 110% | -| Usage Based | Cobra extra por exceso | - -### Enforcement de Limites - -```typescript -// Verificacion de limite de usuarios -async canCreateUser(tenantId: string): Promise { - const subscription = await this.getSubscription(tenantId); - const currentUsers = await this.countUsers(tenantId); - - if (currentUsers >= subscription.limits.maxUsers) { - throw new PaymentRequiredException( - `Limite de usuarios alcanzado (${subscription.limits.maxUsers}). ` + - `Actualiza tu plan para agregar mas usuarios.` - ); - } - return true; -} - -// Verificacion de acceso a modulo -async canAccessModule(tenantId: string, module: string): Promise { - const subscription = await this.getSubscription(tenantId); - - if (!subscription.modules.includes(module)) { - throw new PaymentRequiredException( - `El modulo "${module}" no esta incluido en tu plan. ` + - `Actualiza a ${this.getMinPlanForModule(module)} para acceder.` - ); - } - return true; -} -``` - ---- - -## Reglas de Negocio - -| ID | Regla | -|----|-------| -| RN-001 | Trial expira en 14 dias sin opcion de extension | -| RN-002 | Downgrade no permitido durante periodo de facturacion | -| RN-003 | Upgrade aplica inmediatamente con prorrateo | -| RN-004 | Cancelacion efectiva al fin del periodo pagado | -| RN-005 | Datos se conservan 30 dias despues de cancelacion | -| RN-006 | Exceso de storage: soft limit + cargos adicionales | -| RN-007 | Pago fallido: 7 dias de gracia antes de suspension | -| RN-008 | Enterprise puede negociar limites custom | - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver uso actual vs limites - -```gherkin -Given tenant con plan Professional (50 usuarios, 25GB) - And 35 usuarios activos - And 18GB de storage usado -When Tenant Admin ve la subscripcion -Then muestra "Usuarios: 35/50 (70%)" - And muestra "Storage: 18GB/25GB (72%)" - And muestra grafico de uso -``` - -### Escenario 2: Bloqueo por limite de usuarios - -```gherkin -Given tenant con plan Starter (10 usuarios) - And 10 usuarios activos (100%) -When Admin intenta crear usuario #11 -Then el sistema responde con status 402 - And el mensaje indica limite alcanzado - And sugiere upgrade a Professional -``` - -### Escenario 3: Bloqueo por modulo no incluido - -```gherkin -Given tenant con plan Starter (sin CRM) -When usuario intenta acceder a /api/v1/crm/contacts -Then el sistema responde con status 402 - And el mensaje indica modulo no disponible - And sugiere planes que incluyen CRM -``` - -### Escenario 4: Upgrade de plan - -```gherkin -Given tenant en Starter ($29/mes) al dia 15 del mes -When solicita upgrade a Professional ($99/mes) -Then sistema calcula prorrateo: $35 (15 dias de diferencia) - And usuario paga $35 - And plan cambia inmediatamente - And nuevos limites aplican - And siguiente factura: $99 el dia 1 -``` - -### Escenario 5: Trial expirado - -```gherkin -Given tenant en Trial por 14 dias -When han pasado los 14 dias sin subscription -Then el estado cambia a "trial_expired" - And usuarios no pueden acceder - And datos se conservan - And Admin puede acceder solo para suscribirse -``` - -### Escenario 6: Advertencia de renovacion - -```gherkin -Given subscripcion que vence en 3 dias -When el sistema ejecuta job de notificaciones -Then envia email "Tu subscripcion vence en 3 dias" - And incluye link para verificar metodo de pago -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Mi Subscripcion | -+------------------------------------------------------------------+ -| | -| ┌─────────────────────────────────────────────────────────────┐ | -| │ PLAN ACTUAL: Professional [Cambiar Plan] │ | -| │ │ | -| │ $99 USD / mes Proxima renovacion: 01/01/2026 │ | -| │ Facturacion mensual Metodo de pago: •••• 4242 │ | -| └─────────────────────────────────────────────────────────────┘ | -| | -| USO ACTUAL | -| ┌─────────────────────────────────────────────────────────────┐ | -| │ │ | -| │ Usuarios ████████████████████░░░░░░░░░░ 35/50 (70%) │ | -| │ │ | -| │ Storage █████████████████████████░░░░░ 18/25GB (72%) │ | -| │ │ | -| │ API Calls ██████████░░░░░░░░░░░░░░░░░░░░ 5K/50K (10%) │ | -| │ │ | -| └─────────────────────────────────────────────────────────────┘ | -| | -| MODULOS INCLUIDOS | -| ┌─────────────────────────────────────────────────────────────┐ | -| │ ✓ Auth ✓ Users ✓ Roles ✓ Inventory │ | -| │ ✓ Financial ✓ Reports (Full) ✓ CRM ✓ API Access │ | -| │ │ | -| │ No incluidos: Advanced Analytics, White Label │ | -| │ [Ver Enterprise para estas funciones] │ | -| └─────────────────────────────────────────────────────────────┘ | -| | -| HISTORIAL DE FACTURACION | -| ┌─────────────────────────────────────────────────────────────┐ | -| │ Fecha | Descripcion | Monto | Estado │ | -| │------------|----------------------|---------|-------------- │ | -| │ 01/12/2025 | Professional - Dic | $99.00 | ✓ Pagado │ | -| │ 01/11/2025 | Professional - Nov | $99.00 | ✓ Pagado │ | -| │ 15/10/2025 | Upgrade Starter->Pro | $35.00 | ✓ Pagado │ | -| │ 01/10/2025 | Starter - Oct | $29.00 | ✓ Pagado │ | -| └─────────────────────────────────────────────────────────────┘ | -| | -| [Descargar Facturas] [Cancelar Subscripcion] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Ver subscripcion actual -GET /api/v1/tenant/subscription - -// Response 200 -{ - "plan": { - "id": "plan-professional", - "name": "Professional", - "price": 99, - "currency": "USD", - "interval": "monthly" - }, - "status": "active", - "currentPeriodStart": "2025-12-01", - "currentPeriodEnd": "2025-12-31", - "cancelAtPeriodEnd": false, - "usage": { - "users": { "current": 35, "limit": 50, "percentage": 70 }, - "storage": { "current": 18000000000, "limit": 25000000000, "percentage": 72 }, - "apiCalls": { "current": 5000, "limit": 50000, "percentage": 10 } - }, - "modules": ["auth", "users", "roles", "inventory", "financial", "reports", "crm", "api"], - "paymentMethod": { "type": "card", "last4": "4242", "brand": "visa" } -} - -// Ver planes disponibles -GET /api/v1/subscription/plans - -// Solicitar upgrade -POST /api/v1/tenant/subscription/upgrade -{ "planId": "plan-enterprise" } - -// Cancelar subscripcion -POST /api/v1/tenant/subscription/cancel -{ "reason": "too_expensive", "feedback": "..." } - -// Verificar limite -GET /api/v1/tenant/subscription/check-limit?type=users - -// Response 200 -{ - "type": "users", - "current": 35, - "limit": 50, - "canAdd": true, - "remaining": 15 -} - -// Response 402 (limite alcanzado) -{ - "statusCode": 402, - "error": "Payment Required", - "message": "Limite de usuarios alcanzado", - "upgradeOptions": [ - { "planId": "plan-enterprise", "name": "Enterprise", "newLimit": "unlimited" } - ] -} -``` - -### Modelos de Datos - -```typescript -interface Subscription { - id: string; - tenantId: string; - planId: string; - status: 'trial' | 'active' | 'past_due' | 'canceled' | 'unpaid'; - currentPeriodStart: Date; - currentPeriodEnd: Date; - cancelAtPeriodEnd: boolean; - trialEnd?: Date; - paymentMethodId?: string; -} - -interface Plan { - id: string; - name: string; - price: number; - currency: string; - interval: 'monthly' | 'yearly'; - limits: { - maxUsers: number; - maxStorageBytes: number; - maxApiCalls: number; - }; - modules: string[]; - features: string[]; - isPublic: boolean; -} -``` - -### Limit Enforcement Guard - -```typescript -@Injectable() -export class LimitGuard implements CanActivate { - constructor( - private subscriptionService: SubscriptionService, - private reflector: Reflector, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const limitType = this.reflector.get('checkLimit', context.getHandler()); - if (!limitType) return true; - - const request = context.switchToHttp().getRequest(); - const tenantId = request.tenantId; - - const check = await this.subscriptionService.checkLimit(tenantId, limitType); - - if (!check.canAdd) { - throw new PaymentRequiredException({ - message: `Limite de ${limitType} alcanzado`, - upgradeOptions: check.upgradeOptions, - }); - } - - return true; - } -} - -// Uso en controller -@Post() -@CheckLimit('users') -create(@Body() dto: CreateUserDto) { } -``` - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-TENANT-001 | Tenants existentes | -| Payment Gateway | Stripe/PayPal para pagos | -| Scheduler | Jobs para renovaciones y notificaciones | - ---- - -## Estimacion - -| Tarea | Puntos | -|-------|--------| -| Backend: Subscription service | 4 | -| Backend: Limit enforcement | 4 | -| Backend: Billing integration | 5 | -| Backend: Webhooks de pago | 3 | -| Backend: Tests | 3 | -| Frontend: SubscriptionPage | 4 | -| Frontend: PlanComparison | 3 | -| Frontend: CheckoutFlow | 4 | -| Frontend: Tests | 2 | -| **Total** | **32 SP** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/03-requerimientos/RF-users/INDICE-RF-USER.md b/docs/03-requerimientos/RF-users/INDICE-RF-USER.md deleted file mode 100644 index 085d686..0000000 --- a/docs/03-requerimientos/RF-users/INDICE-RF-USER.md +++ /dev/null @@ -1,260 +0,0 @@ -# Indice de Requerimientos Funcionales - MGN-002 Users - -## Resumen del Modulo - -| Campo | Valor | -|-------|-------| -| **Codigo** | MGN-002 | -| **Nombre** | Users - Gestion de Usuarios | -| **Prioridad** | P0 - Critica | -| **Total RFs** | 5 | -| **Estado** | En documentacion | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion General - -El modulo de usuarios gestiona el ciclo de vida completo de los usuarios del sistema, incluyendo: - -- **CRUD de Usuarios**: Crear, listar, actualizar y eliminar usuarios (admin) -- **Perfil Personal**: Cada usuario gestiona su propia informacion -- **Cambio de Email**: Proceso seguro con verificacion -- **Cambio de Password**: Autoservicio con validaciones -- **Preferencias**: Personalizacion de la experiencia - ---- - -## Lista de Requerimientos Funcionales - -| ID | Nombre | Prioridad | Complejidad | Estado | Story Points | -|----|--------|-----------|-------------|--------|--------------| -| [RF-USER-001](./RF-USER-001.md) | CRUD de Usuarios | P0 | Media | Aprobado | 13 | -| [RF-USER-002](./RF-USER-002.md) | Perfil de Usuario | P1 | Baja | Aprobado | 7 | -| [RF-USER-003](./RF-USER-003.md) | Cambio de Email | P1 | Media | Aprobado | 6 | -| [RF-USER-004](./RF-USER-004.md) | Cambio de Password | P0 | Baja | Aprobado | 4 | -| [RF-USER-005](./RF-USER-005.md) | Preferencias de Usuario | P2 | Baja | Aprobado | 7 | - -**Total Story Points:** 37 - ---- - -## Grafo de Dependencias - -``` -RF-AUTH-001 (Login) - │ - ▼ -RF-USER-001 (CRUD Usuarios) ─────────────────────────┐ - │ │ - ├──────────────────┬──────────────────┐ │ - │ │ │ │ - ▼ ▼ ▼ │ -RF-USER-002 RF-USER-003 RF-USER-004 │ -(Perfil) (Cambio Email) (Cambio Pass) │ - │ │ │ - └────────┬─────────┘ │ - │ │ - ▼ │ - RF-USER-005 ◄───────────────────────────────┘ - (Preferencias) -``` - -### Orden de Implementacion Recomendado - -1. **RF-USER-001** - CRUD de usuarios (base) -2. **RF-USER-004** - Cambio de password (seguridad basica) -3. **RF-USER-002** - Perfil de usuario (autoservicio) -4. **RF-USER-003** - Cambio de email (proceso complejo) -5. **RF-USER-005** - Preferencias (personalizacion) - ---- - -## Endpoints del Modulo - -### Gestion de Usuarios (Admin) - -| Metodo | Endpoint | RF | Descripcion | Permisos | -|--------|----------|-----|-------------|----------| -| POST | `/api/v1/users` | RF-USER-001 | Crear usuario | users:create | -| GET | `/api/v1/users` | RF-USER-001 | Listar usuarios | users:read | -| GET | `/api/v1/users/:id` | RF-USER-001 | Obtener usuario | users:read | -| PATCH | `/api/v1/users/:id` | RF-USER-001 | Actualizar usuario | users:update | -| DELETE | `/api/v1/users/:id` | RF-USER-001 | Eliminar usuario | users:delete | -| POST | `/api/v1/users/:id/activate` | RF-USER-001 | Activar usuario | users:update | -| POST | `/api/v1/users/:id/deactivate` | RF-USER-001 | Desactivar usuario | users:update | - -### Perfil Personal (Self-service) - -| Metodo | Endpoint | RF | Descripcion | -|--------|----------|-----|-------------| -| GET | `/api/v1/users/me` | RF-USER-002 | Obtener mi perfil | -| PATCH | `/api/v1/users/me` | RF-USER-002 | Actualizar mi perfil | -| POST | `/api/v1/users/me/avatar` | RF-USER-002 | Subir avatar | -| DELETE | `/api/v1/users/me/avatar` | RF-USER-002 | Eliminar avatar | -| POST | `/api/v1/users/me/email/request-change` | RF-USER-003 | Solicitar cambio email | -| GET | `/api/v1/users/email/verify-change` | RF-USER-003 | Verificar cambio email | -| POST | `/api/v1/users/me/password` | RF-USER-004 | Cambiar password | -| GET | `/api/v1/users/me/preferences` | RF-USER-005 | Obtener preferencias | -| PATCH | `/api/v1/users/me/preferences` | RF-USER-005 | Actualizar preferencias | -| POST | `/api/v1/users/me/preferences/reset` | RF-USER-005 | Reset preferencias | - ---- - -## Tablas de Base de Datos - -| Tabla | Schema | RF | Descripcion | -|-------|--------|-----|-------------| -| `users` | core_users | RF-USER-001 | Tabla principal de usuarios | -| `user_avatars` | core_users | RF-USER-002 | Historial de avatares | -| `email_change_requests` | core_users | RF-USER-003 | Solicitudes de cambio email | -| `password_history` | core_auth | RF-USER-004 | Historial de passwords | -| `user_preferences` | core_users | RF-USER-005 | Preferencias por usuario | - ---- - -## Modelo de Datos Principal - -```mermaid -erDiagram - users ||--o| user_preferences : "has" - users ||--o{ user_avatars : "has" - users ||--o{ email_change_requests : "requests" - users ||--o{ password_history : "has" - users }o--|| tenants : "belongs to" - - users { - uuid id PK - uuid tenant_id FK - varchar email - varchar password_hash - varchar first_name - varchar last_name - varchar phone - varchar avatar_url - enum status - boolean is_active - timestamptz email_verified_at - timestamptz last_login_at - integer failed_login_attempts - timestamptz locked_until - jsonb metadata - timestamptz created_at - uuid created_by - timestamptz updated_at - uuid updated_by - timestamptz deleted_at - uuid deleted_by - } - - user_preferences { - uuid id PK - uuid user_id FK - uuid tenant_id FK - varchar language - varchar timezone - varchar date_format - varchar time_format - varchar theme - jsonb notifications - jsonb dashboard - timestamptz updated_at - } -``` - ---- - -## Estados de Usuario - -| Estado | Descripcion | Puede Login | -|--------|-------------|-------------| -| `pending_activation` | Recien creado, esperando activacion | No | -| `active` | Usuario activo y funcional | Si | -| `inactive` | Desactivado por admin | No | -| `locked` | Bloqueado por intentos fallidos | No | -| `deleted` | Soft deleted | No | - ---- - -## Criterios de Aceptacion Consolidados - -### Seguridad - -- [ ] Soft delete en lugar de hard delete -- [ ] Solo admins gestionan otros usuarios -- [ ] Email verificado antes de cambio efectivo -- [ ] Password actual requerido para cambios sensibles -- [ ] Historial de passwords para evitar reuso -- [ ] Rate limiting en operaciones sensibles - -### Funcionalidad - -- [ ] CRUD completo con paginacion y filtros -- [ ] Perfil autoservicio funcional -- [ ] Avatar upload con resize automatico -- [ ] Cambio de email con doble verificacion -- [ ] Cambio de password con politica de complejidad -- [ ] Preferencias persistentes y aplicadas - -### Auditoria - -- [ ] created_by/updated_by en todas las operaciones -- [ ] deleted_by en soft deletes -- [ ] Historial de cambios de email -- [ ] Historial de passwords - ---- - -## Estimacion Total - -| Capa | Story Points | -|------|--------------| -| Database | 6 | -| Backend | 15 | -| Frontend | 16 | -| **Total** | **37** | - ---- - -## Riesgos Identificados - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Spam de cambio de email | Media | Bajo | Rate limiting 3/dia | -| Storage de avatares lleno | Baja | Medio | Cleanup job, limites | -| Performance en listados | Media | Medio | Paginacion, indices | -| Enumeracion de usuarios | Baja | Alto | Permisos estrictos | - ---- - -## Referencias - -### Documentacion Relacionada - -- [DDL-SPEC-core_users.md](../../02-modelado/database-design/DDL-SPEC-core_users.md) - Especificacion de base de datos -- [ET-users-backend.md](../../02-modelado/especificaciones-tecnicas/ET-users-backend.md) - Especificacion tecnica backend -- [TP-users.md](../../04-test-plans/TP-users.md) - Plan de pruebas - -### Dependencias con Otros Modulos - -- **MGN-001 (Auth)**: Autenticacion y tokens -- **MGN-003 (Roles)**: Asignacion de roles a usuarios -- **MGN-004 (Tenants)**: Aislamiento multi-tenant - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 5 RFs | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-users/RF-USER-001.md b/docs/03-requerimientos/RF-users/RF-USER-001.md deleted file mode 100644 index 9bfbf7e..0000000 --- a/docs/03-requerimientos/RF-users/RF-USER-001.md +++ /dev/null @@ -1,333 +0,0 @@ -# RF-USER-001: CRUD de Usuarios - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-USER-001 | -| **Modulo** | MGN-002 | -| **Nombre Modulo** | Users - Gestion de Usuarios | -| **Prioridad** | P0 | -| **Complejidad** | Media | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir la gestion completa del ciclo de vida de usuarios, incluyendo crear, leer, actualizar y eliminar (soft delete) usuarios dentro de un tenant. Solo usuarios con permisos administrativos pueden gestionar otros usuarios. - -### Contexto de Negocio - -La gestion de usuarios es fundamental para: -- Controlar quien accede al sistema -- Asignar roles y permisos apropiados -- Mantener registro de empleados/usuarios del tenant -- Cumplir con politicas de seguridad y auditoria - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El sistema debe permitir crear usuarios con datos basicos (email, nombre, apellido) -- [x] **CA-002:** El sistema debe generar password temporal o enviar invitacion por email -- [x] **CA-003:** El sistema debe validar unicidad de email dentro del tenant -- [x] **CA-004:** El sistema debe permitir listar usuarios con paginacion y filtros -- [x] **CA-005:** El sistema debe permitir buscar usuarios por nombre, email o rol -- [x] **CA-006:** El sistema debe permitir actualizar datos de usuario -- [x] **CA-007:** El sistema debe implementar soft delete (no eliminar fisicamente) -- [x] **CA-008:** El sistema debe permitir activar/desactivar usuarios -- [x] **CA-009:** El sistema debe registrar quien creo/modifico cada usuario (auditoria) -- [x] **CA-010:** Solo admins pueden crear/modificar/eliminar usuarios - -### Ejemplos de Verificacion - -```gherkin -Scenario: Crear usuario exitosamente - Given un administrador autenticado - When crea un nuevo usuario con email "nuevo@empresa.com" - And nombre "Juan" y apellido "Perez" - Then el sistema crea el usuario con estado "pending_activation" - And envia email de invitacion al usuario - And registra created_by con el ID del admin - And responde con status 201 - -Scenario: Crear usuario con email duplicado - Given un usuario existente con email "existente@empresa.com" - When un admin intenta crear otro usuario con el mismo email - Then el sistema responde con status 409 - And el mensaje es "El email ya esta registrado" - -Scenario: Listar usuarios con filtros - Given 50 usuarios en el sistema - When un admin solicita GET /api/v1/users?page=1&limit=10&role=admin - Then el sistema retorna los primeros 10 usuarios con rol admin - And incluye metadata de paginacion (total, pages, hasNext) - -Scenario: Soft delete de usuario - Given un usuario activo con ID "user-123" - When un admin elimina el usuario - Then el campo deleted_at se establece con la fecha actual - And el campo deleted_by se establece con el ID del admin - And el usuario ya no aparece en listados normales - And el usuario no puede hacer login -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | Email unico por tenant | UNIQUE(tenant_id, email) | -| RN-002 | Email en formato valido | Regex validation | -| RN-003 | Nombre y apellido requeridos | NOT NULL, min 2 chars | -| RN-004 | Soft delete en lugar de hard delete | deleted_at NOT NULL | -| RN-005 | Solo admins gestionan usuarios | RBAC permission check | -| RN-006 | Usuario no puede eliminarse a si mismo | Validacion en backend | -| RN-007 | Password temporal expira en 24 horas | Validacion en activacion | -| RN-008 | Auditoria de cambios obligatoria | created_by, updated_by | - -### Estados de Usuario - -``` - ┌─────────────────┐ - │ CREATED │ - │ (pending_activ) │ - └────────┬────────┘ - │ Usuario activa cuenta - ▼ -┌─────────────┐ ┌─────────────────┐ ┌─────────────┐ -│ LOCKED │◄────►│ ACTIVE │◄────►│ INACTIVE │ -│ (bloqueado) │ │ (activo) │ │ (inactivo) │ -└─────────────┘ └────────┬────────┘ └─────────────┘ - │ Admin elimina - ▼ - ┌─────────────────┐ - │ DELETED │ - │ (soft delete) │ - └─────────────────┘ -``` - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Schema | crear | `core_users` | -| Tabla | crear | `users` - tabla principal | -| Columna | - | `id` UUID PK | -| Columna | - | `tenant_id` UUID FK | -| Columna | - | `email` VARCHAR(255) | -| Columna | - | `password_hash` VARCHAR(255) | -| Columna | - | `first_name` VARCHAR(100) | -| Columna | - | `last_name` VARCHAR(100) | -| Columna | - | `phone` VARCHAR(20) | -| Columna | - | `status` ENUM | -| Columna | - | `is_active` BOOLEAN | -| Columna | - | `email_verified_at` TIMESTAMPTZ | -| Columna | - | `last_login_at` TIMESTAMPTZ | -| Columna | - | `failed_login_attempts` INTEGER | -| Columna | - | `locked_until` TIMESTAMPTZ | -| Columna | - | `created_by` UUID FK | -| Columna | - | `updated_by` UUID FK | -| Columna | - | `deleted_at` TIMESTAMPTZ | -| Columna | - | `deleted_by` UUID FK | -| Indice | crear | `idx_users_tenant_email` UNIQUE | -| Indice | crear | `idx_users_tenant_status` | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Controller | crear | `UsersController` | -| Service | crear | `UsersService` | -| Method | crear | `create()` | -| Method | crear | `findAll()` | -| Method | crear | `findOne()` | -| Method | crear | `update()` | -| Method | crear | `remove()` (soft delete) | -| Method | crear | `activate()` | -| Method | crear | `deactivate()` | -| DTO | crear | `CreateUserDto` | -| DTO | crear | `UpdateUserDto` | -| DTO | crear | `UserResponseDto` | -| DTO | crear | `UserListQueryDto` | -| Guard | usar | `RolesGuard` | -| Endpoint | crear | `POST /api/v1/users` | -| Endpoint | crear | `GET /api/v1/users` | -| Endpoint | crear | `GET /api/v1/users/:id` | -| Endpoint | crear | `PATCH /api/v1/users/:id` | -| Endpoint | crear | `DELETE /api/v1/users/:id` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Pagina | crear | `UsersListPage` | -| Pagina | crear | `UserDetailPage` | -| Pagina | crear | `UserCreatePage` | -| Pagina | crear | `UserEditPage` | -| Componente | crear | `UsersTable` | -| Componente | crear | `UserForm` | -| Componente | crear | `UserCard` | -| Service | crear | `usersService` | -| Store | crear | `usersStore` | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-AUTH-001 | Login | Para autenticacion | -| RF-AUTH-002 | JWT Tokens | Para autorizacion | - -### Dependencias Relacionadas - -| ID | Requerimiento | Relacion | -|----|---------------|----------| -| RF-USER-002 | Perfil de usuario | Extiende datos de usuario | -| RF-ROLE-001 | Asignacion de roles | Usa tabla users | - ---- - -## Especificaciones Tecnicas - -### Modelo de Datos Simplificado - -```typescript -interface User { - id: string; - tenantId: string; - email: string; - passwordHash: string; - firstName: string; - lastName: string; - phone?: string; - avatarUrl?: string; - status: UserStatus; - isActive: boolean; - emailVerifiedAt?: Date; - lastLoginAt?: Date; - failedLoginAttempts: number; - lockedUntil?: Date; - metadata?: Record; - createdAt: Date; - createdBy?: string; - updatedAt: Date; - updatedBy?: string; - deletedAt?: Date; - deletedBy?: string; -} - -enum UserStatus { - PENDING_ACTIVATION = 'pending_activation', - ACTIVE = 'active', - INACTIVE = 'inactive', - LOCKED = 'locked', -} -``` - -### Flujo de Creacion de Usuario - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. Admin envia POST /api/v1/users │ -│ Body: { email, firstName, lastName, roleIds } │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 2. Validaciones │ -│ - Email formato valido │ -│ - Email no existe en tenant │ -│ - Admin tiene permiso users:create │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 3. Crear usuario │ -│ - status = 'pending_activation' │ -│ - is_active = false │ -│ - created_by = admin.id │ -│ - Generar activation token │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 4. Asignar roles │ -│ - Insertar en user_roles │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 5. Enviar email de invitacion │ -│ - Link: /activate?token={activation_token} │ -│ - Token expira en 24 horas │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 6. Responder con usuario creado │ -│ - Status 201 │ -│ - Body: UserResponseDto (sin password) │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Datos de Prueba - -| Escenario | Entrada | Resultado | -|-----------|---------|-----------| -| Crear usuario valido | email, firstName, lastName | 201, usuario creado | -| Email duplicado | email existente | 409, "Email ya registrado" | -| Email invalido | "notanemail" | 400, "Email invalido" | -| Sin permiso | Usuario no admin | 403, "Permiso denegado" | -| Listar usuarios | page=1, limit=10 | 200, lista paginada | -| Buscar por email | search=john@ | 200, usuarios filtrados | -| Actualizar usuario | PATCH con datos | 200, usuario actualizado | -| Soft delete | DELETE /users/:id | 200, deleted_at set | -| Eliminar a si mismo | DELETE propio ID | 400, "No puede eliminarse" | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 3 | Schema y tabla users | -| Backend | 5 | CRUD completo con validaciones | -| Frontend | 5 | 4 paginas, componentes, store | -| **Total** | **13** | | - ---- - -## Notas Adicionales - -- Implementar soft delete para mantener integridad referencial -- El email de invitacion debe usar template personalizable -- Considerar bulk import de usuarios via CSV -- Implementar export de lista de usuarios -- Los usuarios eliminados se pueden restaurar (admin) - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-users/RF-USER-002.md b/docs/03-requerimientos/RF-users/RF-USER-002.md deleted file mode 100644 index e4269d1..0000000 --- a/docs/03-requerimientos/RF-users/RF-USER-002.md +++ /dev/null @@ -1,314 +0,0 @@ -# RF-USER-002: Perfil de Usuario - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-USER-002 | -| **Modulo** | MGN-002 | -| **Nombre Modulo** | Users - Gestion de Usuarios | -| **Prioridad** | P1 | -| **Complejidad** | Baja | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a cada usuario ver y editar su propio perfil, incluyendo informacion personal, foto de perfil y datos de contacto. A diferencia del CRUD de usuarios (RF-USER-001), el perfil es autoservicio - cada usuario gestiona su propia informacion. - -### Contexto de Negocio - -El perfil de usuario permite: -- Personalizacion de la experiencia -- Informacion de contacto actualizada -- Identidad visual mediante avatar -- Datos para notificaciones y comunicacion - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El usuario debe poder ver su perfil completo -- [x] **CA-002:** El usuario debe poder editar nombre y apellido -- [x] **CA-003:** El usuario debe poder editar telefono -- [x] **CA-004:** El usuario debe poder subir foto de perfil (avatar) -- [x] **CA-005:** El usuario NO debe poder cambiar su email directamente -- [x] **CA-006:** El sistema debe validar formato de telefono -- [x] **CA-007:** El sistema debe redimensionar imagenes de avatar automaticamente -- [x] **CA-008:** El perfil debe mostrar informacion de la cuenta (fecha registro, ultimo login) - -### Ejemplos de Verificacion - -```gherkin -Scenario: Ver perfil propio - Given un usuario autenticado - When accede a GET /api/v1/users/me - Then el sistema retorna su perfil completo - And incluye firstName, lastName, email, phone, avatarUrl - And incluye createdAt, lastLoginAt - And NO incluye passwordHash ni datos sensibles - -Scenario: Actualizar nombre - Given un usuario autenticado - When actualiza su nombre a "Carlos" - Then el sistema guarda el cambio - And updated_by se establece con su propio ID - And responde con el perfil actualizado - -Scenario: Subir avatar - Given un usuario autenticado - And una imagen JPG de 2MB - When sube la imagen como avatar - Then el sistema redimensiona a 200x200 px - And genera thumbnail de 50x50 px - And almacena en storage (S3/local) - And actualiza avatarUrl en el usuario - -Scenario: Imagen muy grande - Given un usuario autenticado - And una imagen de 15MB - When intenta subir como avatar - Then el sistema responde con status 400 - And el mensaje es "Imagen excede tamaño maximo (10MB)" -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | Solo el propio usuario edita su perfil | user.id == request.userId | -| RN-002 | Email no editable desde perfil | Campo readonly | -| RN-003 | Avatar max 10MB | File size validation | -| RN-004 | Formatos permitidos: JPG, PNG, WebP | MIME type check | -| RN-005 | Avatar redimensionado a 200x200 | Image processing | -| RN-006 | Telefono formato E.164 | Regex +[0-9]{10,15} | -| RN-007 | Nombre min 2, max 100 caracteres | String validation | - -### Campos Editables vs No Editables - -| Campo | Editable | Notas | -|-------|----------|-------| -| firstName | Si | Min 2 chars | -| lastName | Si | Min 2 chars | -| phone | Si | Formato E.164 | -| avatarUrl | Si | Via upload | -| email | No | Requiere proceso separado | -| status | No | Solo admin | -| roles | No | Solo admin | -| tenantId | No | Inmutable | - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Tabla | usar | `users` - ya existe | -| Columna | agregar | `avatar_url` VARCHAR(500) | -| Columna | agregar | `avatar_thumbnail_url` VARCHAR(500) | -| Tabla | crear | `user_avatars` - historial de avatares | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Controller | agregar | `UsersController.getProfile()` | -| Controller | agregar | `UsersController.updateProfile()` | -| Controller | agregar | `UsersController.uploadAvatar()` | -| Method | crear | `UsersService.getProfile()` | -| Method | crear | `UsersService.updateProfile()` | -| Method | crear | `AvatarService.upload()` | -| Method | crear | `AvatarService.resize()` | -| DTO | crear | `UpdateProfileDto` | -| DTO | crear | `ProfileResponseDto` | -| Endpoint | crear | `GET /api/v1/users/me` | -| Endpoint | crear | `PATCH /api/v1/users/me` | -| Endpoint | crear | `POST /api/v1/users/me/avatar` | -| Endpoint | crear | `DELETE /api/v1/users/me/avatar` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Pagina | crear | `ProfilePage` | -| Componente | crear | `ProfileForm` | -| Componente | crear | `AvatarUploader` | -| Componente | crear | `AvatarCropper` | -| Service | crear | `profileService` | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-USER-001 | CRUD Usuarios | Tabla users | -| RF-AUTH-001 | Login | Autenticacion | - -### Dependencias Externas - -| Servicio | Descripcion | -|----------|-------------| -| Storage | S3, MinIO o filesystem para avatares | -| Image Processing | Sharp o similar para resize | - ---- - -## Especificaciones Tecnicas - -### Endpoint GET /api/v1/users/me - -```typescript -// Response 200 -{ - "id": "uuid", - "email": "user@example.com", - "firstName": "Juan", - "lastName": "Perez", - "phone": "+521234567890", - "avatarUrl": "https://storage.erp.com/avatars/uuid-200.jpg", - "avatarThumbnailUrl": "https://storage.erp.com/avatars/uuid-50.jpg", - "status": "active", - "emailVerifiedAt": "2025-01-01T00:00:00Z", - "lastLoginAt": "2025-12-05T10:30:00Z", - "createdAt": "2025-01-01T00:00:00Z", - "tenant": { - "id": "tenant-uuid", - "name": "Empresa XYZ" - }, - "roles": [ - { "id": "role-uuid", "name": "admin" } - ] -} -``` - -### Endpoint PATCH /api/v1/users/me - -```typescript -// Request -{ - "firstName": "Carlos", - "lastName": "Lopez", - "phone": "+521234567890" -} - -// Response 200 -{ - // ProfileResponseDto actualizado -} -``` - -### Endpoint POST /api/v1/users/me/avatar - -```typescript -// Request -// Content-Type: multipart/form-data -// Field: avatar (file) - -// Response 200 -{ - "avatarUrl": "https://storage.erp.com/avatars/uuid-200.jpg", - "avatarThumbnailUrl": "https://storage.erp.com/avatars/uuid-50.jpg" -} -``` - -### Procesamiento de Avatar - -```typescript -// avatar.service.ts -async uploadAvatar(userId: string, file: Express.Multer.File): Promise { - // 1. Validar archivo - this.validateFile(file); // size, mime type - - // 2. Generar nombres unicos - const filename = `${userId}-${Date.now()}`; - - // 3. Procesar imagen - const mainBuffer = await sharp(file.buffer) - .resize(200, 200, { fit: 'cover' }) - .jpeg({ quality: 85 }) - .toBuffer(); - - const thumbBuffer = await sharp(file.buffer) - .resize(50, 50, { fit: 'cover' }) - .jpeg({ quality: 80 }) - .toBuffer(); - - // 4. Subir a storage - const mainUrl = await this.storage.upload(`avatars/${filename}-200.jpg`, mainBuffer); - const thumbUrl = await this.storage.upload(`avatars/${filename}-50.jpg`, thumbBuffer); - - // 5. Eliminar avatar anterior (opcional) - await this.deleteOldAvatar(userId); - - // 6. Actualizar usuario - await this.usersRepository.update(userId, { - avatarUrl: mainUrl, - avatarThumbnailUrl: thumbUrl, - }); - - return { avatarUrl: mainUrl, avatarThumbnailUrl: thumbUrl }; -} -``` - ---- - -## Datos de Prueba - -| Escenario | Entrada | Resultado | -|-----------|---------|-----------| -| Ver perfil | GET /users/me | 200, perfil completo | -| Actualizar nombre | firstName: "Carlos" | 200, actualizado | -| Telefono invalido | phone: "123" | 400, "Formato invalido" | -| Avatar JPG valido | imagen 1MB | 200, URLs generadas | -| Avatar muy grande | imagen 15MB | 400, "Excede limite" | -| Avatar formato invalido | archivo .pdf | 400, "Formato no permitido" | -| Eliminar avatar | DELETE /users/me/avatar | 200, avatar eliminado | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 1 | Columnas avatar | -| Backend | 3 | Profile endpoints + avatar | -| Frontend | 3 | Profile page + avatar uploader | -| **Total** | **7** | | - ---- - -## Notas Adicionales - -- Considerar CDN para servir avatares -- Implementar cache de avatares -- Avatar por defecto basado en iniciales (fallback) -- Considerar gravatar como fallback -- Rate limiting en upload de avatares - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-users/RF-USER-003.md b/docs/03-requerimientos/RF-users/RF-USER-003.md deleted file mode 100644 index f28aafc..0000000 --- a/docs/03-requerimientos/RF-users/RF-USER-003.md +++ /dev/null @@ -1,332 +0,0 @@ -# RF-USER-003: Cambio de Email - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-USER-003 | -| **Modulo** | MGN-002 | -| **Nombre Modulo** | Users - Gestion de Usuarios | -| **Prioridad** | P1 | -| **Complejidad** | Media | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a los usuarios cambiar su direccion de email de forma segura, requiriendo verificacion del nuevo email antes de completar el cambio. Este proceso protege contra cambios no autorizados y mantiene la integridad de las comunicaciones. - -### Contexto de Negocio - -El cambio de email es sensible porque: -- El email es el identificador principal de login -- Se usa para recuperacion de password -- Se usa para notificaciones importantes -- Debe ser verificado antes del cambio efectivo - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El usuario debe poder solicitar cambio de email -- [x] **CA-002:** El sistema debe enviar verificacion al NUEVO email -- [x] **CA-003:** El cambio no se aplica hasta verificar el nuevo email -- [x] **CA-004:** El sistema debe validar que el nuevo email no exista en el tenant -- [x] **CA-005:** El token de verificacion expira en 24 horas -- [x] **CA-006:** El sistema debe notificar al email ANTERIOR sobre el cambio -- [x] **CA-007:** El usuario debe confirmar con su password actual -- [x] **CA-008:** Despues del cambio, todas las sesiones se invalidan - -### Ejemplos de Verificacion - -```gherkin -Scenario: Solicitar cambio de email - Given un usuario con email "viejo@empresa.com" - When solicita cambiar a "nuevo@empresa.com" - And confirma con su password actual - Then el sistema valida que "nuevo@empresa.com" no existe - And genera token de verificacion - And envia email de verificacion a "nuevo@empresa.com" - And el email actual permanece sin cambios - And responde con status 200 - -Scenario: Verificar nuevo email - Given una solicitud de cambio pendiente - And un token de verificacion valido - When el usuario hace clic en el link de verificacion - Then el sistema actualiza el email del usuario - And invalida todas las sesiones activas - And envia notificacion al email anterior - And redirige al login - -Scenario: Token de verificacion expirado - Given un token de verificacion de mas de 24 horas - When el usuario intenta usarlo - Then el sistema responde con error - And el mensaje es "Token expirado, solicita nuevo cambio" - -Scenario: Nuevo email ya existe - Given un email "existente@empresa.com" ya registrado - When un usuario intenta cambiar a ese email - Then el sistema responde con status 409 - And el mensaje es "Email no disponible" -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | Requiere password actual para solicitar cambio | Verificacion bcrypt | -| RN-002 | Nuevo email debe ser unico en tenant | UNIQUE constraint | -| RN-003 | Token de verificacion expira en 24 horas | expires_at check | -| RN-004 | Solo una solicitud activa a la vez | Invalidar anteriores | -| RN-005 | Notificar al email anterior | Email de seguridad | -| RN-006 | Logout-all despues del cambio | Revocar tokens | -| RN-007 | Nuevo email formato valido | Regex validation | - -### Flujo de Cambio de Email - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. Usuario solicita cambio │ -│ POST /api/v1/users/me/email/request-change │ -│ Body: { newEmail, currentPassword } │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 2. Validaciones │ -│ - Password correcto │ -│ - Nuevo email formato valido │ -│ - Nuevo email no existe en tenant │ -│ - No hay solicitud pendiente │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 3. Crear solicitud │ -│ - Guardar en email_change_requests │ -│ - Generar token (32 bytes, hex) │ -│ - expires_at = now + 24 hours │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 4. Enviar email de verificacion │ -│ To: nuevo@email.com │ -│ Link: /verify-email-change?token={token} │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 5. Usuario hace clic en link │ -│ GET /api/v1/users/email/verify-change?token={token} │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 6. Aplicar cambio │ -│ - Actualizar users.email │ -│ - Marcar solicitud como completada │ -│ - Invalidar todas las sesiones │ -│ - Enviar notificacion al email anterior │ -└───────────────────────────┬─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────────┐ -│ 7. Redirigir al login │ -│ - Usuario debe iniciar sesion con nuevo email │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Tabla | crear | `email_change_requests` | -| Columna | - | `id` UUID PK | -| Columna | - | `user_id` UUID FK | -| Columna | - | `tenant_id` UUID FK | -| Columna | - | `current_email` VARCHAR(255) | -| Columna | - | `new_email` VARCHAR(255) | -| Columna | - | `token_hash` VARCHAR(255) | -| Columna | - | `expires_at` TIMESTAMPTZ | -| Columna | - | `completed_at` TIMESTAMPTZ | -| Columna | - | `created_at` TIMESTAMPTZ | -| Indice | crear | `idx_email_change_user` | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Controller | agregar | `UsersController.requestEmailChange()` | -| Controller | agregar | `UsersController.verifyEmailChange()` | -| Method | crear | `UsersService.requestEmailChange()` | -| Method | crear | `UsersService.verifyEmailChange()` | -| DTO | crear | `RequestEmailChangeDto` | -| Entity | crear | `EmailChangeRequest` | -| Endpoint | crear | `POST /api/v1/users/me/email/request-change` | -| Endpoint | crear | `GET /api/v1/users/email/verify-change` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Componente | crear | `ChangeEmailForm` | -| Pagina | crear | `VerifyEmailChangePage` | -| Modal | crear | `ConfirmPasswordModal` | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-USER-001 | CRUD Usuarios | Tabla users | -| RF-AUTH-004 | Logout | Para logout-all | - -### Dependencias Externas - -| Servicio | Descripcion | -|----------|-------------| -| Email Service | Envio de verificacion | - ---- - -## Especificaciones Tecnicas - -### Endpoint POST /api/v1/users/me/email/request-change - -```typescript -// Request -{ - "newEmail": "nuevo@empresa.com", - "currentPassword": "MiPasswordActual123!" -} - -// Response 200 -{ - "message": "Se ha enviado un email de verificacion a nuevo@empresa.com", - "expiresAt": "2025-12-06T10:30:00Z" -} - -// Response 400 - Password incorrecto -{ - "statusCode": 400, - "message": "Password incorrecto" -} - -// Response 409 - Email existe -{ - "statusCode": 409, - "message": "Email no disponible" -} -``` - -### Endpoint GET /api/v1/users/email/verify-change - -```typescript -// Request -GET /api/v1/users/email/verify-change?token=abc123... - -// Response 200 (redirect) -// Redirect to: /login?emailChanged=true - -// Response 400 - Token invalido -{ - "statusCode": 400, - "message": "Token invalido o expirado" -} -``` - -### Template de Email - Verificacion - -```html -Asunto: Verifica tu nuevo email - ERP Suite - -

Hola {{firstName}},

- -

Recibimos una solicitud para cambiar tu email de:

-

{{currentEmail}} a {{newEmail}}

- -

Haz clic en el siguiente enlace para confirmar el cambio:

-Verificar nuevo email - -

Este enlace expira en 24 horas.

- -

Si no solicitaste este cambio, ignora este email y considera -cambiar tu password por seguridad.

-``` - -### Template de Email - Notificacion al Email Anterior - -```html -Asunto: Tu email ha sido cambiado - ERP Suite - -

Hola {{firstName}},

- -

Te informamos que el email de tu cuenta ha sido cambiado.

- -

Email anterior: {{oldEmail}}

-

Email nuevo: {{newEmail}}

-

Fecha: {{changeDate}}

- -

Si no realizaste este cambio, contacta inmediatamente a soporte.

-``` - ---- - -## Datos de Prueba - -| Escenario | Entrada | Resultado | -|-----------|---------|-----------| -| Solicitud valida | newEmail, password correcto | 200, email enviado | -| Password incorrecto | password erroneo | 400, "Password incorrecto" | -| Email ya existe | email de otro usuario | 409, "Email no disponible" | -| Email invalido | "notanemail" | 400, "Email invalido" | -| Verificar token valido | token < 24h | 200, email cambiado | -| Token expirado | token > 24h | 400, "Token expirado" | -| Token ya usado | solicitud completada | 400, "Token ya utilizado" | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 1 | Tabla email_change_requests | -| Backend | 3 | Endpoints, validaciones, emails | -| Frontend | 2 | Form y pagina verificacion | -| **Total** | **6** | | - ---- - -## Notas Adicionales - -- Considerar periodo de gracia donde se puede revertir el cambio -- Implementar notificacion push ademas de email -- Rate limiting en solicitudes (max 3 por dia) -- Log de todos los cambios de email para auditoria - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-users/RF-USER-004.md b/docs/03-requerimientos/RF-users/RF-USER-004.md deleted file mode 100644 index 5c7d002..0000000 --- a/docs/03-requerimientos/RF-users/RF-USER-004.md +++ /dev/null @@ -1,362 +0,0 @@ -# RF-USER-004: Cambio de Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-USER-004 | -| **Modulo** | MGN-002 | -| **Nombre Modulo** | Users - Gestion de Usuarios | -| **Prioridad** | P0 | -| **Complejidad** | Baja | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a los usuarios cambiar su contraseña de forma segura, requiriendo la contraseña actual para autorizar el cambio. Este proceso es diferente a la recuperacion de password (RF-AUTH-005) ya que el usuario conoce su password actual. - -### Contexto de Negocio - -El cambio de password es necesario para: -- Cumplir con politicas de rotacion de passwords -- Responder a sospechas de compromiso -- Buenas practicas de seguridad -- Requisitos de compliance - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El usuario debe proporcionar su password actual para cambiar -- [x] **CA-002:** El nuevo password debe cumplir politica de complejidad -- [x] **CA-003:** El nuevo password no puede ser igual a los ultimos 5 -- [x] **CA-004:** El sistema debe invalidar todas las otras sesiones (opcional) -- [x] **CA-005:** El sistema debe notificar via email del cambio -- [x] **CA-006:** El sistema debe registrar el cambio en password_history -- [x] **CA-007:** La sesion actual puede mantenerse activa - -### Ejemplos de Verificacion - -```gherkin -Scenario: Cambio de password exitoso - Given un usuario autenticado - When proporciona password actual correcto - And nuevo password "NuevoPass123!" - And el nuevo password cumple requisitos - Then el sistema actualiza el password - And guarda en password_history - And envia email de notificacion - And opcionalmente invalida otras sesiones - And responde con status 200 - -Scenario: Password actual incorrecto - Given un usuario autenticado - When proporciona password actual incorrecto - Then el sistema responde con status 400 - And el mensaje es "Password actual incorrecto" - -Scenario: Nuevo password no cumple requisitos - Given un usuario autenticado - When el nuevo password es "abc123" - Then el sistema responde con status 400 - And lista los requisitos no cumplidos - -Scenario: Password igual a anterior - Given un usuario que uso "MiPass123!" hace 2 meses - When intenta cambiar a "MiPass123!" nuevamente - Then el sistema responde con status 400 - And el mensaje es "No puedes reusar passwords anteriores" -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | Password actual requerido | Verificacion bcrypt | -| RN-002 | Nuevo password min 8 caracteres | String length | -| RN-003 | Nuevo password requiere mayuscula | Regex [A-Z] | -| RN-004 | Nuevo password requiere minuscula | Regex [a-z] | -| RN-005 | Nuevo password requiere numero | Regex [0-9] | -| RN-006 | Nuevo password requiere especial | Regex [@$!%*?&] | -| RN-007 | No reusar ultimos 5 passwords | Check password_history | -| RN-008 | Nuevo != Actual | Comparacion | - -### Politica de Password - -``` -Requisitos minimos: -├── Longitud: 8-128 caracteres -├── Al menos 1 mayuscula (A-Z) -├── Al menos 1 minuscula (a-z) -├── Al menos 1 numero (0-9) -├── Al menos 1 caracter especial (!@#$%^&*) -├── No puede contener el email del usuario -└── No puede ser igual a los ultimos 5 passwords -``` - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Tabla | usar | `users` - actualizar password_hash | -| Tabla | usar | `password_history` - registrar cambio | -| Tabla | usar | `session_history` - registrar evento | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Controller | agregar | `UsersController.changePassword()` | -| Method | crear | `UsersService.changePassword()` | -| Method | usar | `PasswordService.validatePolicy()` | -| Method | usar | `PasswordService.checkHistory()` | -| DTO | crear | `ChangePasswordDto` | -| Endpoint | crear | `POST /api/v1/users/me/password` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Componente | crear | `ChangePasswordForm` | -| Componente | usar | `PasswordStrengthIndicator` | -| Pagina | agregar | Seccion en ProfilePage | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-USER-001 | CRUD Usuarios | Tabla users | -| RF-AUTH-001 | Login | Autenticacion | - -### Reutiliza de - -| ID | Requerimiento | Elementos | -|----|---------------|-----------| -| RF-AUTH-005 | Password Recovery | password_history, validaciones | - ---- - -## Especificaciones Tecnicas - -### Endpoint POST /api/v1/users/me/password - -```typescript -// Request -{ - "currentPassword": "MiPasswordActual123!", - "newPassword": "MiNuevoPassword456!", - "confirmPassword": "MiNuevoPassword456!", - "logoutOtherSessions": true // opcional, default false -} - -// Response 200 -{ - "message": "Password actualizado exitosamente", - "sessionsInvalidated": 2 // si logoutOtherSessions = true -} - -// Response 400 - Password actual incorrecto -{ - "statusCode": 400, - "message": "Password actual incorrecto" -} - -// Response 400 - No cumple politica -{ - "statusCode": 400, - "message": "El password no cumple los requisitos", - "errors": [ - "Debe incluir al menos una mayuscula", - "Debe incluir al menos un caracter especial" - ] -} - -// Response 400 - Password reutilizado -{ - "statusCode": 400, - "message": "No puedes usar un password que hayas usado anteriormente" -} -``` - -### Implementacion del Service - -```typescript -// users.service.ts -async changePassword( - userId: string, - dto: ChangePasswordDto, -): Promise { - const user = await this.usersRepository.findOne({ where: { id: userId } }); - - // 1. Verificar password actual - const isCurrentValid = await bcrypt.compare(dto.currentPassword, user.passwordHash); - if (!isCurrentValid) { - throw new BadRequestException('Password actual incorrecto'); - } - - // 2. Verificar que nuevo != actual - if (dto.currentPassword === dto.newPassword) { - throw new BadRequestException('El nuevo password debe ser diferente al actual'); - } - - // 3. Validar confirmacion - if (dto.newPassword !== dto.confirmPassword) { - throw new BadRequestException('Los passwords no coinciden'); - } - - // 4. Validar politica de password - const policyErrors = this.passwordService.validatePolicy(dto.newPassword, user.email); - if (policyErrors.length > 0) { - throw new BadRequestException({ - message: 'El password no cumple los requisitos', - errors: policyErrors, - }); - } - - // 5. Verificar historial - const isReused = await this.passwordService.isPasswordReused(userId, dto.newPassword); - if (isReused) { - throw new BadRequestException('No puedes usar un password que hayas usado anteriormente'); - } - - // 6. Hashear nuevo password - const newHash = await bcrypt.hash(dto.newPassword, 12); - - // 7. Guardar en historial - await this.passwordHistoryRepository.save({ - userId, - tenantId: user.tenantId, - passwordHash: newHash, - }); - - // 8. Actualizar usuario - await this.usersRepository.update(userId, { - passwordHash: newHash, - updatedBy: userId, - }); - - // 9. Registrar evento - await this.sessionHistoryRepository.save({ - userId, - tenantId: user.tenantId, - action: 'password_change', - }); - - // 10. Invalidar otras sesiones si se solicita - let sessionsInvalidated = 0; - if (dto.logoutOtherSessions) { - sessionsInvalidated = await this.tokenService.revokeAllUserTokens( - userId, - 'password_change', - ); - } - - // 11. Enviar email de notificacion - await this.emailService.sendPasswordChangedEmail(user.email, user.firstName); - - return { - message: 'Password actualizado exitosamente', - sessionsInvalidated, - }; -} -``` - -### Validacion de Politica - -```typescript -// password.service.ts -validatePolicy(password: string, email: string): string[] { - const errors: string[] = []; - - if (password.length < 8) { - errors.push('Debe tener al menos 8 caracteres'); - } - if (password.length > 128) { - errors.push('No puede exceder 128 caracteres'); - } - if (!/[A-Z]/.test(password)) { - errors.push('Debe incluir al menos una mayuscula'); - } - if (!/[a-z]/.test(password)) { - errors.push('Debe incluir al menos una minuscula'); - } - if (!/[0-9]/.test(password)) { - errors.push('Debe incluir al menos un numero'); - } - if (!/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)) { - errors.push('Debe incluir al menos un caracter especial'); - } - if (password.toLowerCase().includes(email.split('@')[0].toLowerCase())) { - errors.push('No puede contener tu nombre de usuario'); - } - - return errors; -} -``` - ---- - -## Datos de Prueba - -| Escenario | Entrada | Resultado | -|-----------|---------|-----------| -| Cambio exitoso | currentPassword correcto, newPassword valido | 200, actualizado | -| Password actual incorrecto | currentPassword erroneo | 400, "Password actual incorrecto" | -| Passwords no coinciden | newPassword != confirmPassword | 400, "No coinciden" | -| Password muy corto | newPassword: "Ab1!" | 400, "Min 8 caracteres" | -| Sin mayuscula | newPassword: "password123!" | 400, "Requiere mayuscula" | -| Password reutilizado | newPassword en historial | 400, "No reusar" | -| Con logout otras sesiones | logoutOtherSessions: true | 200, sesiones cerradas | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 0 | Usa tablas existentes | -| Backend | 2 | Endpoint + validaciones | -| Frontend | 2 | Form + strength indicator | -| **Total** | **4** | | - ---- - -## Notas Adicionales - -- Considerar forzar cambio de password cada N dias (configurable) -- Implementar indicador de fuerza de password en tiempo real -- Rate limiting: max 3 intentos por hora -- Log detallado para auditoria de seguridad -- Considerar 2FA antes de cambio de password (futuro) - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/03-requerimientos/RF-users/RF-USER-005.md b/docs/03-requerimientos/RF-users/RF-USER-005.md deleted file mode 100644 index ec85a0d..0000000 --- a/docs/03-requerimientos/RF-users/RF-USER-005.md +++ /dev/null @@ -1,370 +0,0 @@ -# RF-USER-005: Preferencias de Usuario - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | RF-USER-005 | -| **Modulo** | MGN-002 | -| **Nombre Modulo** | Users - Gestion de Usuarios | -| **Prioridad** | P2 | -| **Complejidad** | Baja | -| **Estado** | Aprobado | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Descripcion - -El sistema debe permitir a cada usuario configurar sus preferencias personales, incluyendo idioma, zona horaria, formato de fecha/hora, tema visual y preferencias de notificaciones. Estas configuraciones personalizan la experiencia del usuario sin afectar a otros usuarios del tenant. - -### Contexto de Negocio - -Las preferencias de usuario permiten: -- Experiencia personalizada por usuario -- Soporte multi-idioma -- Adaptacion a zonas horarias locales -- Control sobre notificaciones recibidas -- Accesibilidad (tema oscuro, tamano de fuente) - ---- - -## Criterios de Aceptacion - -- [x] **CA-001:** El usuario debe poder seleccionar idioma preferido -- [x] **CA-002:** El usuario debe poder configurar zona horaria -- [x] **CA-003:** El usuario debe poder elegir formato de fecha (DD/MM/YYYY, MM/DD/YYYY, etc.) -- [x] **CA-004:** El usuario debe poder elegir formato de hora (12h, 24h) -- [x] **CA-005:** El usuario debe poder elegir tema (claro, oscuro, sistema) -- [x] **CA-006:** El usuario debe poder configurar preferencias de notificaciones -- [x] **CA-007:** Las preferencias deben aplicarse inmediatamente -- [x] **CA-008:** Las preferencias deben persistir entre sesiones - -### Ejemplos de Verificacion - -```gherkin -Scenario: Cambiar idioma - Given un usuario con idioma "es" (español) - When cambia su preferencia a "en" (ingles) - Then el sistema guarda la preferencia - And la interfaz cambia a ingles inmediatamente - And las fechas se formatean en ingles - -Scenario: Configurar zona horaria - Given un usuario en Mexico (America/Mexico_City) - When configura su zona horaria - Then todas las fechas/horas se muestran en esa zona - And los eventos del calendario se ajustan - -Scenario: Preferencias de notificaciones - Given un usuario con notificaciones por email activadas - When desactiva "notificaciones de marketing" - Then deja de recibir ese tipo de emails - And mantiene otras notificaciones activas - -Scenario: Tema oscuro - Given un usuario con tema claro - When activa tema oscuro - Then la interfaz cambia a colores oscuros - And la preferencia se mantiene al recargar -``` - ---- - -## Reglas de Negocio - -| ID | Regla | Validacion | -|----|-------|------------| -| RN-001 | Idiomas soportados: es, en, pt | Enum validation | -| RN-002 | Zona horaria debe ser valida IANA | Timezone validation | -| RN-003 | Preferencias son por usuario, no por tenant | user_id scope | -| RN-004 | Valores por defecto del tenant si no hay preferencia | Fallback logic | -| RN-005 | Tema "sistema" sigue preferencia del OS | CSS media query | - -### Preferencias Disponibles - -```typescript -interface UserPreferences { - // Localizacion - language: 'es' | 'en' | 'pt'; - timezone: string; // IANA timezone - dateFormat: 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY-MM-DD'; - timeFormat: '12h' | '24h'; - currency: string; // ISO 4217 - numberFormat: 'es-MX' | 'en-US' | 'pt-BR'; - - // Apariencia - theme: 'light' | 'dark' | 'system'; - sidebarCollapsed: boolean; - compactMode: boolean; - fontSize: 'small' | 'medium' | 'large'; - - // Notificaciones - notifications: { - email: { - enabled: boolean; - digest: 'instant' | 'daily' | 'weekly'; - marketing: boolean; - security: boolean; - updates: boolean; - }; - push: { - enabled: boolean; - sound: boolean; - }; - inApp: { - enabled: boolean; - desktop: boolean; - }; - }; - - // Dashboard - dashboard: { - defaultView: string; - widgets: string[]; - }; -} -``` - ---- - -## Impacto en Capas - -### Database - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Tabla | crear | `user_preferences` | -| Columna | - | `id` UUID PK | -| Columna | - | `user_id` UUID FK UNIQUE | -| Columna | - | `tenant_id` UUID FK | -| Columna | - | `language` VARCHAR(5) | -| Columna | - | `timezone` VARCHAR(50) | -| Columna | - | `date_format` VARCHAR(20) | -| Columna | - | `time_format` VARCHAR(5) | -| Columna | - | `theme` VARCHAR(10) | -| Columna | - | `notifications` JSONB | -| Columna | - | `dashboard` JSONB | -| Columna | - | `metadata` JSONB | -| Columna | - | `updated_at` TIMESTAMPTZ | - -### Backend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Controller | crear | `PreferencesController` | -| Service | crear | `PreferencesService` | -| Method | crear | `getPreferences()` | -| Method | crear | `updatePreferences()` | -| Method | crear | `resetPreferences()` | -| DTO | crear | `UpdatePreferencesDto` | -| DTO | crear | `PreferencesResponseDto` | -| Entity | crear | `UserPreferences` | -| Endpoint | crear | `GET /api/v1/users/me/preferences` | -| Endpoint | crear | `PATCH /api/v1/users/me/preferences` | -| Endpoint | crear | `POST /api/v1/users/me/preferences/reset` | - -### Frontend - -| Elemento | Accion | Descripcion | -|----------|--------|-------------| -| Pagina | crear | `PreferencesPage` | -| Componente | crear | `LanguageSelector` | -| Componente | crear | `TimezoneSelector` | -| Componente | crear | `ThemeToggle` | -| Componente | crear | `NotificationSettings` | -| Store | crear | `preferencesStore` | -| Hook | crear | `usePreferences` | -| Context | crear | `PreferencesContext` | - ---- - -## Dependencias - -### Depende de (Bloqueantes) - -| ID | Requerimiento | Estado | -|----|---------------|--------| -| RF-USER-001 | CRUD Usuarios | Tabla users | - -### Dependencias Relacionadas - -| ID | Requerimiento | Relacion | -|----|---------------|----------| -| RF-SETTINGS-001 | Tenant Settings | Valores por defecto | - ---- - -## Especificaciones Tecnicas - -### Endpoint GET /api/v1/users/me/preferences - -```typescript -// Response 200 -{ - "language": "es", - "timezone": "America/Mexico_City", - "dateFormat": "DD/MM/YYYY", - "timeFormat": "24h", - "currency": "MXN", - "numberFormat": "es-MX", - "theme": "dark", - "sidebarCollapsed": false, - "compactMode": false, - "fontSize": "medium", - "notifications": { - "email": { - "enabled": true, - "digest": "daily", - "marketing": false, - "security": true, - "updates": true - }, - "push": { - "enabled": true, - "sound": true - }, - "inApp": { - "enabled": true, - "desktop": false - } - }, - "dashboard": { - "defaultView": "overview", - "widgets": ["sales", "inventory", "tasks"] - } -} -``` - -### Endpoint PATCH /api/v1/users/me/preferences - -```typescript -// Request - Actualizacion parcial -{ - "theme": "light", - "notifications": { - "email": { - "marketing": false - } - } -} - -// Response 200 -{ - // PreferencesResponseDto completo actualizado -} -``` - -### Merge de Preferencias - -```typescript -// preferences.service.ts -async updatePreferences( - userId: string, - updates: Partial, -): Promise { - let preferences = await this.preferencesRepository.findOne({ - where: { userId }, - }); - - if (!preferences) { - // Crear con defaults del tenant - const tenantDefaults = await this.getTenantDefaults(userId); - preferences = this.preferencesRepository.create({ - userId, - ...tenantDefaults, - }); - } - - // Deep merge para objetos anidados (notifications, dashboard) - const merged = deepMerge(preferences, updates); - - return this.preferencesRepository.save(merged); -} -``` - -### Aplicacion en Frontend - -```typescript -// PreferencesContext.tsx -export const PreferencesProvider: React.FC = ({ children }) => { - const [preferences, setPreferences] = useState(null); - - useEffect(() => { - loadPreferences(); - }, []); - - useEffect(() => { - if (preferences) { - // Aplicar tema - document.documentElement.setAttribute('data-theme', preferences.theme); - - // Aplicar idioma - i18n.changeLanguage(preferences.language); - - // Configurar moment/dayjs timezone - dayjs.tz.setDefault(preferences.timezone); - } - }, [preferences]); - - return ( - - {children} - - ); -}; -``` - ---- - -## Datos de Prueba - -| Escenario | Entrada | Resultado | -|-----------|---------|-----------| -| Obtener preferencias | GET /preferences | 200, preferencias o defaults | -| Cambiar idioma | language: "en" | 200, idioma actualizado | -| Zona horaria invalida | timezone: "Invalid/Zone" | 400, "Zona horaria invalida" | -| Tema valido | theme: "dark" | 200, tema actualizado | -| Tema invalido | theme: "purple" | 400, "Valor no permitido" | -| Desactivar notificaciones | notifications.email.enabled: false | 200, actualizado | -| Reset preferencias | POST /reset | 200, defaults del tenant | - ---- - -## Estimacion - -| Capa | Story Points | Notas | -|------|--------------|-------| -| Database | 1 | Tabla user_preferences | -| Backend | 2 | CRUD preferencias | -| Frontend | 4 | UI de configuracion + contexto | -| **Total** | **7** | | - ---- - -## Notas Adicionales - -- Las preferencias se cargan al inicio de sesion y se cachean -- Cambios de tema deben ser instantaneos (sin reload) -- Considerar preferencias sincronizadas entre dispositivos -- Exportar/importar preferencias para usuarios -- Preferencias de accesibilidad (alto contraste, reducir animaciones) - ---- - -## Historial de Cambios - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | - ---- - -## Aprobaciones - -| Rol | Nombre | Fecha | Firma | -|-----|--------|-------|-------| -| Analista | System | 2025-12-05 | [x] | -| Tech Lead | - | - | [ ] | -| Product Owner | - | - | [ ] | diff --git a/docs/05-user-stories/_legacy_backup/MGN-001/BACKLOG-MGN001.md b/docs/05-user-stories/_legacy_backup/MGN-001/BACKLOG-MGN001.md deleted file mode 100644 index b94be00..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-001/BACKLOG-MGN001.md +++ /dev/null @@ -1,162 +0,0 @@ -# Backlog del Modulo MGN-001: Auth - -## Resumen - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-001 | -| **Nombre** | Auth - Autenticacion | -| **Total User Stories** | 4 | -| **Total Story Points** | 29 | -| **Estado** | En documentacion | -| **Fecha** | 2025-12-05 | - ---- - -## User Stories - -### Sprint 1 - Foundation (21 SP) - -| ID | Nombre | SP | Prioridad | Estado | Asignado | -|----|--------|-----|-----------|--------|----------| -| [US-MGN001-001](./US-MGN001-001.md) | Login con Email y Password | 8 | P0 | Ready | - | -| [US-MGN001-002](./US-MGN001-002.md) | Logout y Cierre de Sesion | 5 | P0 | Ready | - | -| [US-MGN001-003](./US-MGN001-003.md) | Renovacion Automatica de Tokens | 8 | P0 | Ready | - | - -### Sprint 2 - Enhancement (8 SP) - -| ID | Nombre | SP | Prioridad | Estado | Asignado | -|----|--------|-----|-----------|--------|----------| -| [US-MGN001-004](./US-MGN001-004.md) | Recuperacion de Password | 8 | P1 | Ready | - | - ---- - -## Roadmap Visual - -``` -Sprint 1 Sprint 2 -├─────────────────────────────────┼─────────────────────────────────┤ -│ US-001: Login [8 SP] │ US-004: Password Recovery [8 SP]│ -│ US-002: Logout [5 SP] │ │ -│ US-003: Token Refresh [8 SP] │ │ -├─────────────────────────────────┼─────────────────────────────────┤ -│ Total: 21 SP │ Total: 8 SP │ -└─────────────────────────────────┴─────────────────────────────────┘ -``` - ---- - -## Dependencias entre Stories - -``` -US-MGN001-001 (Login) - │ - ├──────────────────┐ - │ │ - ▼ ▼ -US-MGN001-002 (Logout) US-MGN001-003 (Refresh) - │ - │ - ▼ -US-MGN001-004 (Password Recovery) - │ - └── Usa logout-all -``` - ---- - -## Criterios de Aceptacion del Modulo - -### Funcionalidad - -- [ ] Los usuarios pueden autenticarse con email y password -- [ ] Los usuarios pueden cerrar sesion de forma segura -- [ ] Las sesiones se renuevan automaticamente -- [ ] Los usuarios pueden recuperar su password - -### Seguridad - -- [ ] Passwords hasheados con bcrypt (salt rounds = 12) -- [ ] Tokens JWT firmados con RS256 -- [ ] Refresh token en cookie httpOnly -- [ ] Deteccion de token replay -- [ ] Rate limiting implementado -- [ ] Bloqueo de cuenta por intentos fallidos -- [ ] No se revela existencia de emails - -### Performance - -- [ ] Login < 500ms -- [ ] Refresh < 200ms -- [ ] Blacklist en Redis - -### Auditoria - -- [ ] Todos los logins registrados -- [ ] Todos los logouts registrados -- [ ] Intentos fallidos registrados -- [ ] Cambios de password registrados - ---- - -## Metricas del Modulo - -### Cobertura de Codigo - -| Capa | Objetivo | Actual | -|------|----------|--------| -| Backend Services | 80% | - | -| Backend Controllers | 70% | - | -| Frontend Services | 70% | - | -| Frontend Components | 60% | - | - -### Performance - -| Endpoint | Objetivo | P95 | -|----------|----------|-----| -| POST /auth/login | < 500ms | - | -| POST /auth/refresh | < 200ms | - | -| POST /auth/logout | < 200ms | - | -| POST /password/request-reset | < 1000ms | - | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Vulnerabilidad en JWT | Media | Alto | Code review, testing seguridad | -| Token replay attack | Baja | Alto | Deteccion de familia, rotacion | -| Email delivery failure | Media | Medio | Retry queue, monitoring | -| Brute force en login | Media | Medio | Rate limiting, CAPTCHA | - ---- - -## Definition of Done del Modulo - -- [ ] Todas las User Stories completadas -- [ ] Tests unitarios > 80% coverage -- [ ] Tests e2e pasando -- [ ] Documentacion Swagger completa -- [ ] Code review aprobado -- [ ] Security review aprobado -- [ ] Performance review aprobado -- [ ] Despliegue en staging exitoso -- [ ] UAT aprobado - ---- - -## Notas del Product Owner - -- El login es bloqueante para todo el proyecto -- Password recovery puede esperar a Sprint 2 -- Priorizar seguridad sobre features adicionales -- Considerar 2FA para fase posterior (no en MVP) - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 US | diff --git a/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-001.md b/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-001.md deleted file mode 100644 index 24f47fe..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-001.md +++ /dev/null @@ -1,296 +0,0 @@ -# US-MGN001-001: Login con Email y Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN001-001 | -| **Modulo** | MGN-001 Auth | -| **Sprint** | Sprint 1 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario del sistema ERP -**Quiero** poder iniciar sesion con mi email y contraseña -**Para** acceder a las funcionalidades del sistema de forma segura - ---- - -## Descripcion - -El usuario necesita un mecanismo seguro para autenticarse en el sistema utilizando sus credenciales (email y contraseña). Al autenticarse exitosamente, recibira tokens JWT que le permitiran acceder a los recursos protegidos. - -### Contexto - -- Primera interaccion del usuario con el sistema -- Puerta de entrada a todas las funcionalidades -- Base de la seguridad del sistema - ---- - -## Criterios de Aceptacion - -### Escenario 1: Login exitoso - -```gherkin -Given un usuario registrado con email "user@example.com" - And password "SecurePass123!" - And el usuario no esta bloqueado - And el usuario esta activo -When el usuario envia sus credenciales al endpoint /api/v1/auth/login -Then el sistema responde con status 200 - And el body contiene "accessToken" de tipo string - And el body contiene "refreshToken" de tipo string - And el body contiene "user" con id, email, firstName, lastName, roles - And se establece cookie httpOnly "refresh_token" - And se registra el login en session_history -``` - -### Escenario 2: Login con email incorrecto - -```gherkin -Given un email "noexiste@example.com" que no existe en el sistema -When el usuario intenta hacer login con ese email -Then el sistema responde con status 401 - And el mensaje es "Credenciales invalidas" - And NO se revela que el email no existe - And se registra el intento fallido en login_attempts -``` - -### Escenario 3: Login con password incorrecto - -```gherkin -Given un usuario registrado con email "user@example.com" - And el password correcto es "SecurePass123!" -When el usuario intenta hacer login con password "wrongpassword" -Then el sistema responde con status 401 - And el mensaje es "Credenciales invalidas" - And se incrementa failed_login_attempts del usuario - And se registra el intento fallido en login_attempts -``` - -### Escenario 4: Cuenta bloqueada por intentos fallidos - -```gherkin -Given un usuario con email "user@example.com" - And el usuario tiene 5 intentos fallidos de login - And el campo locked_until es una fecha futura -When el usuario intenta hacer login con credenciales correctas -Then el sistema responde con status 423 - And el mensaje indica "Cuenta bloqueada" - And se indica el tiempo restante de bloqueo -``` - -### Escenario 5: Cuenta inactiva - -```gherkin -Given un usuario con email "inactive@example.com" - And el campo is_active del usuario es false -When el usuario intenta hacer login -Then el sistema responde con status 403 - And el mensaje es "Cuenta inactiva" -``` - -### Escenario 6: Validacion de campos - -```gherkin -Given un request al endpoint de login -When el email no es un email valido -Then el sistema responde con status 400 - And el mensaje indica "Email invalido" - -When el password tiene menos de 8 caracteres -Then el sistema responde con status 400 - And el mensaje indica "Password debe tener minimo 8 caracteres" - -When el email esta vacio -Then el sistema responde con status 400 - And el mensaje indica "Email es requerido" -``` - -### Escenario 7: Desbloqueo automatico - -```gherkin -Given un usuario con cuenta bloqueada - And el tiempo de bloqueo (30 minutos) ha pasado -When el usuario intenta hacer login con credenciales correctas -Then el sistema permite el login - And resetea failed_login_attempts a 0 - And limpia locked_until -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -| | -| ╔═══════════════════════════╗ | -| ║ INICIAR SESION ║ | -| ╚═══════════════════════════╝ | -| | -| Correo electronico | -| +---------------------------+ | -| | user@example.com | | -| +---------------------------+ | -| | -| Contraseña | -| +---------------------------+ | -| | •••••••••••• | [👁] | -| +---------------------------+ | -| | -| [ ] Recordar sesion | -| | -| [===== INICIAR SESION =====] | -| | -| ¿Olvidaste tu contraseña? | -| | -+------------------------------------------------------------------+ - -Estados de UI: -┌─────────────────────────────────────────────────────────────────┐ -│ Estado: Loading │ -│ - Boton deshabilitado │ -│ - Spinner visible en boton │ -│ - Campos deshabilitados │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ Estado: Error │ -│ - Toast rojo con mensaje de error │ -│ - Campos con borde rojo si invalidos │ -│ - Texto de ayuda debajo del campo │ -└─────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────┐ -│ Estado: Bloqueado │ -│ - Mensaje: "Cuenta bloqueada. Intenta en XX minutos" │ -│ - Enlace a "¿Necesitas ayuda?" │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API - -```typescript -// Request -POST /api/v1/auth/login -Content-Type: application/json -X-Tenant-Id: optional-if-single-tenant - -{ - "email": "user@example.com", - "password": "SecurePass123!" -} - -// Response 200 -{ - "accessToken": "eyJhbGciOiJSUzI1NiIs...", - "refreshToken": "eyJhbGciOiJSUzI1NiIs...", - "tokenType": "Bearer", - "expiresIn": 900, - "user": { - "id": "uuid", - "email": "user@example.com", - "firstName": "John", - "lastName": "Doe", - "roles": ["admin"] - } -} - -// Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict; Max-Age=604800 -``` - -### Validaciones - -| Campo | Regla | Mensaje Error | -|-------|-------|---------------| -| email | Required, IsEmail | "Email es requerido" / "Email invalido" | -| password | Required, MinLength(8), MaxLength(128) | "Password es requerido" / "Password debe tener minimo 8 caracteres" | - -### Seguridad - -- Bcrypt con salt rounds = 12 -- Rate limiting: 10 requests/minuto por IP -- No revelar si email existe -- Tokens firmados con RS256 - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/auth/login implementado -- [ ] Validaciones de DTO funcionando -- [ ] Login exitoso retorna tokens JWT -- [ ] Login fallido registra intento -- [ ] Bloqueo despues de 5 intentos -- [ ] Desbloqueo automatico despues de 30 min -- [ ] Cookie httpOnly para refresh token -- [ ] Registro en session_history -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Documentacion Swagger actualizada -- [ ] Code review aprobado - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| Tabla users | Con campos email, password_hash, is_active | -| Tabla login_attempts | Para registro de intentos | -| Tabla session_history | Para auditoria | -| TokenService | Para generar tokens JWT | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| US-MGN001-002 | Logout (necesita sesion) | -| US-MGN001-003 | Refresh token (necesita login) | -| Todas las features | Requieren autenticacion | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: AuthService.login() | 4h | -| Backend: Validaciones y errores | 2h | -| Backend: Tests unitarios | 3h | -| Frontend: LoginPage | 4h | -| Frontend: authStore | 2h | -| Frontend: Tests | 2h | -| **Total** | **17h** | - ---- - -## Referencias - -- [RF-AUTH-001](../../01-requerimientos/RF-auth/RF-AUTH-001.md) - Requerimiento funcional -- [DDL-SPEC-core_auth](../../02-modelado/database-design/DDL-SPEC-core_auth.md) - Esquema BD -- [ET-auth-backend](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Spec tecnica - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-002.md b/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-002.md deleted file mode 100644 index 59860a1..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-002.md +++ /dev/null @@ -1,261 +0,0 @@ -# US-MGN001-002: Logout y Cierre de Sesion - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN001-002 | -| **Modulo** | MGN-001 Auth | -| **Sprint** | Sprint 1 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 5 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema ERP -**Quiero** poder cerrar mi sesion de forma segura -**Para** proteger mi cuenta cuando dejo de usar el sistema o en dispositivos compartidos - ---- - -## Descripcion - -El usuario necesita poder cerrar su sesion actual, revocando los tokens de acceso para que no puedan ser reutilizados. Tambien debe poder cerrar todas sus sesiones activas en caso de sospecha de compromiso de cuenta. - -### Contexto - -- Seguridad en dispositivos compartidos -- Cumplimiento de politicas corporativas -- Proteccion contra robo de tokens - ---- - -## Criterios de Aceptacion - -### Escenario 1: Logout exitoso - -```gherkin -Given un usuario autenticado con sesion activa - And tiene un access token valido - And tiene un refresh token en cookie -When el usuario hace POST /api/v1/auth/logout -Then el sistema responde con status 200 - And el mensaje es "Sesion cerrada exitosamente" - And el refresh token es revocado en BD - And el access token es agregado a la blacklist - And la cookie refresh_token es eliminada - And se registra el logout en session_history -``` - -### Escenario 2: Logout de todas las sesiones - -```gherkin -Given un usuario autenticado - And tiene 3 sesiones activas en diferentes dispositivos -When el usuario hace POST /api/v1/auth/logout-all -Then el sistema responde con status 200 - And el mensaje es "Todas las sesiones han sido cerradas" - And el response incluye "sessionsRevoked": 3 - And TODOS los refresh tokens del usuario son revocados - And se registra el logout_all en session_history -``` - -### Escenario 3: Logout sin autenticacion - -```gherkin -Given un usuario sin token de acceso -When intenta hacer logout -Then el sistema responde con status 401 - And el mensaje es "Token requerido" -``` - -### Escenario 4: Logout con token expirado - -```gherkin -Given un usuario con access token expirado - And tiene refresh token valido en cookie -When intenta hacer logout -Then el sistema permite el logout usando solo el refresh token - And responde con status 200 -``` - -### Escenario 5: Verificacion post-logout - -```gherkin -Given un usuario que acaba de hacer logout -When intenta acceder a un endpoint protegido - With el access token anterior -Then el sistema responde con status 401 - And el mensaje es "Token revocado" -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] [Usuario ▼] | -+------------------------------------------------------------------+ - ┌─────────────────┐ - │ Mi Perfil │ - │ Configuracion │ - │ ─────────────── │ - │ Cerrar Sesion │ ← Click - │ Cerrar Todas │ - └─────────────────┘ - -┌──────────────────────────────────────────────────────────────────┐ -│ CONFIRMACION DE LOGOUT │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ ¿Estas seguro que deseas cerrar sesion? │ -│ │ -│ [ Cancelar ] [ Cerrar Sesion ] │ -│ │ -└──────────────────────────────────────────────────────────────────┘ - -┌──────────────────────────────────────────────────────────────────┐ -│ CERRAR TODAS LAS SESIONES │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ ⚠️ Esta accion cerrara tu sesion en todos los dispositivos. │ -│ │ -│ Sesiones activas: 3 │ -│ - Chrome (Windows) - Hace 2 horas │ -│ - Firefox (Mac) - Hace 1 dia │ -│ - Safari (iPhone) - Ahora │ -│ │ -│ [ Cancelar ] [ Cerrar Todas ] │ -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API - -```typescript -// Logout individual -POST /api/v1/auth/logout -Authorization: Bearer eyJhbGciOiJSUzI1NiIs... -Cookie: refresh_token=eyJhbGciOiJSUzI1NiIs... - -// Response 200 -{ - "message": "Sesion cerrada exitosamente" -} -// Set-Cookie: refresh_token=; Max-Age=0; HttpOnly; Secure; SameSite=Strict - -// Logout all -POST /api/v1/auth/logout-all -Authorization: Bearer eyJhbGciOiJSUzI1NiIs... - -// Response 200 -{ - "message": "Todas las sesiones han sido cerradas", - "sessionsRevoked": 3 -} -``` - -### Flujo de Logout - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Frontend │ │ Backend │ │ Redis │ -└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ - │ │ │ - │ POST /logout │ │ - │───────────────────>│ │ - │ │ │ - │ │ Revoke refresh │ - │ │ token in DB │ - │ │ │ - │ │ Blacklist access │ - │ │───────────────────>│ - │ │ │ - │ │ Delete cookie │ - │ │ │ - │ 200 OK │ │ - │<───────────────────│ │ - │ │ │ - │ Clear local state │ │ - │ Redirect to login │ │ - │ │ │ -``` - -### Seguridad - -- Blacklist en Redis con TTL -- Logout idempotente (no falla si ya esta logged out) -- Rate limiting: 10 requests/minuto - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/auth/logout implementado -- [ ] Endpoint POST /api/v1/auth/logout-all implementado -- [ ] Refresh token revocado en BD -- [ ] Access token blacklisteado en Redis -- [ ] Cookie eliminada correctamente -- [ ] Registro en session_history -- [ ] Frontend limpia estado local -- [ ] Frontend redirige a login -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN001-001 | Login (sesion activa) | -| BlacklistService | Para invalidar tokens | -| Redis | Para almacenar blacklist | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| - | No bloquea otras historias | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: logout() | 2h | -| Backend: logoutAll() | 2h | -| Backend: Tests | 2h | -| Frontend: Logout button | 1h | -| Frontend: Confirmation modal | 2h | -| Frontend: Tests | 1h | -| **Total** | **10h** | - ---- - -## Referencias - -- [RF-AUTH-004](../../01-requerimientos/RF-auth/RF-AUTH-004.md) - Requerimiento funcional -- [ET-auth-backend](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Spec tecnica - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-003.md b/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-003.md deleted file mode 100644 index cc8c917..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-003.md +++ /dev/null @@ -1,300 +0,0 @@ -# US-MGN001-003: Renovacion Automatica de Tokens - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN001-003 | -| **Modulo** | MGN-001 Auth | -| **Sprint** | Sprint 1 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema ERP -**Quiero** que mi sesion se renueve automaticamente mientras estoy usando el sistema -**Para** no tener que re-autenticarme constantemente y tener una experiencia de usuario fluida - ---- - -## Descripcion - -El sistema debe renovar automaticamente los tokens de acceso antes de que expiren, utilizando el refresh token. Esto permite mantener sesiones de larga duracion (hasta 7 dias) mientras los access tokens mantienen una vida corta (15 minutos) por seguridad. - -### Contexto - -- Access tokens expiran en 15 minutos por seguridad -- Los usuarios no deben ser interrumpidos mientras trabajan -- El proceso debe ser transparente para el usuario - ---- - -## Criterios de Aceptacion - -### Escenario 1: Refresh automatico exitoso - -```gherkin -Given un usuario autenticado trabajando en el sistema - And su access token expira en menos de 1 minuto - And su refresh token es valido -When el frontend detecta que el token esta por expirar -Then el frontend envia automaticamente POST /api/v1/auth/refresh - And recibe nuevos access y refresh tokens - And actualiza los tokens en memoria - And la cookie httpOnly es actualizada - And el usuario NO es interrumpido -``` - -### Escenario 2: Refresh con token valido (manual) - -```gherkin -Given un usuario con refresh token valido -When hace POST /api/v1/auth/refresh -Then el sistema valida el refresh token - And genera un nuevo par de tokens - And el refresh token anterior es marcado como usado - And el nuevo refresh token pertenece a la misma familia - And responde con status 200 -``` - -### Escenario 3: Refresh token expirado - -```gherkin -Given un usuario con refresh token expirado (>7 dias) -When intenta renovar tokens -Then el sistema responde con status 401 - And el mensaje es "Refresh token expirado" - And el usuario es redirigido al login -``` - -### Escenario 4: Deteccion de token replay - -```gherkin -Given un refresh token que ya fue usado para renovar - And el sistema genero un nuevo token despues de usarlo -When alguien intenta usar el refresh token viejo -Then el sistema detecta el reuso - And invalida TODA la familia de tokens - And responde con status 401 - And el mensaje es "Sesion comprometida. Por favor inicia sesion." - And todas las sesiones del usuario son cerradas -``` - -### Escenario 5: Refresh sin token - -```gherkin -Given un request sin refresh token -When intenta renovar tokens -Then el sistema responde con status 400 - And el mensaje es "Refresh token requerido" -``` - -### Escenario 6: Multiples requests simultaneos - -```gherkin -Given un usuario con token por expirar - And el frontend hace 3 requests simultaneos de refresh -When los requests llegan al servidor -Then solo el primero obtiene nuevos tokens - And los siguientes reciben los nuevos tokens (idempotente) - OR los siguientes reciben error y deben reintentar con el nuevo token -``` - ---- - -## Mockup / Wireframe - -``` -El proceso es INVISIBLE para el usuario. No hay UI directa. - -┌──────────────────────────────────────────────────────────────────┐ -│ Indicador de sesion (opcional en header) │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ [🔒 Sesion activa] ← Verde: sesion renovada │ -│ │ -│ [⚠️ Renovando...] ← Amarillo: refresh en progreso │ -│ │ -│ [❌ Sesion expirada] ← Rojo: necesita login │ -│ │ -└──────────────────────────────────────────────────────────────────┘ - -Flujo de sesion expirada: -┌──────────────────────────────────────────────────────────────────┐ -│ SESION EXPIRADA │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ Tu sesion ha expirado. Por favor inicia sesion nuevamente. │ -│ │ -│ [ Ir a Login ] │ -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API - -```typescript -// Request -POST /api/v1/auth/refresh -Cookie: refresh_token=eyJhbGciOiJSUzI1NiIs... - -// O en body (fallback) -{ - "refreshToken": "eyJhbGciOiJSUzI1NiIs..." -} - -// Response 200 -{ - "accessToken": "eyJhbGciOiJSUzI1NiIs...", - "refreshToken": "eyJhbGciOiJSUzI1NiIs...", - "tokenType": "Bearer", - "expiresIn": 900 -} -// Set-Cookie: refresh_token=nuevo...; HttpOnly; Secure; SameSite=Strict -``` - -### Logica de Refresh en Frontend - -```typescript -// Token refresh interceptor (pseudo-code) -class TokenRefreshService { - private refreshing = false; - private refreshPromise: Promise | null = null; - - async checkAndRefresh(): Promise { - const accessToken = this.getAccessToken(); - const expiresAt = this.decodeExpiration(accessToken); - const now = Date.now(); - const oneMinute = 60 * 1000; - - // Renovar si expira en menos de 1 minuto - if (expiresAt - now < oneMinute) { - await this.refresh(); - } - } - - async refresh(): Promise { - // Evitar multiples refreshes simultaneos - if (this.refreshing) { - return this.refreshPromise; - } - - this.refreshing = true; - this.refreshPromise = this.doRefresh(); - - try { - await this.refreshPromise; - } finally { - this.refreshing = false; - this.refreshPromise = null; - } - } - - private async doRefresh(): Promise { - try { - const response = await api.post('/auth/refresh'); - this.setTokens(response.data); - } catch (error) { - // Refresh failed, redirect to login - this.clearTokens(); - router.push('/login'); - throw error; - } - } -} -``` - -### Rotacion de Tokens (Token Family) - -``` -Login exitoso: - └── RT1 (family: ABC) ← Activo - -Refresh 1: - ├── RT1 (family: ABC) → isUsed: true, replacedBy: RT2 - └── RT2 (family: ABC) ← Activo - -Refresh 2: - ├── RT1 (family: ABC) → isUsed: true, replacedBy: RT2 - ├── RT2 (family: ABC) → isUsed: true, replacedBy: RT3 - └── RT3 (family: ABC) ← Activo - -Token Replay (RT1 reutilizado): - ├── RT1 (family: ABC) → ALERTA! ya esta usado - ├── RT2 (family: ABC) → REVOCADO por seguridad - └── RT3 (family: ABC) → REVOCADO por seguridad -``` - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/auth/refresh implementado -- [ ] Rotacion de tokens funcionando -- [ ] Deteccion de token replay -- [ ] Revocacion de familia en caso de reuso -- [ ] Cookie actualizada en cada refresh -- [ ] Frontend: interceptor de refresh automatico -- [ ] Frontend: manejo de sesion expirada -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| US-MGN001-001 | Login (obtener tokens iniciales) | -| Tabla refresh_tokens | Con campos family_id, is_used, replaced_by | -| Redis | Para rate limiting | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| Todas las features | Dependen de sesion activa | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: refreshTokens() | 4h | -| Backend: Token rotation logic | 3h | -| Backend: Token replay detection | 2h | -| Backend: Tests | 3h | -| Frontend: Refresh interceptor | 3h | -| Frontend: Token storage | 2h | -| Frontend: Tests | 2h | -| **Total** | **19h** | - ---- - -## Referencias - -- [RF-AUTH-003](../../01-requerimientos/RF-auth/RF-AUTH-003.md) - Requerimiento funcional -- [RF-AUTH-002](../../01-requerimientos/RF-auth/RF-AUTH-002.md) - Estructura de tokens -- [ET-auth-backend](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Spec tecnica - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-004.md b/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-004.md deleted file mode 100644 index f446c5b..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-001/US-MGN001-004.md +++ /dev/null @@ -1,391 +0,0 @@ -# US-MGN001-004: Recuperacion de Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN001-004 | -| **Modulo** | MGN-001 Auth | -| **Sprint** | Sprint 2 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario que olvido su contraseña -**Quiero** poder recuperar el acceso a mi cuenta mediante un proceso seguro -**Para** no quedar bloqueado del sistema sin necesidad de contactar soporte - ---- - -## Descripcion - -El usuario que olvido su contraseña debe poder solicitar un enlace de recuperacion por email. Este enlace le permitira establecer una nueva contraseña de forma segura, invalidando el acceso anterior. - -### Contexto - -- Los usuarios olvidan contraseñas con frecuencia -- El proceso debe ser autoservicio -- Debe ser seguro contra ataques de enumeracion de emails - ---- - -## Criterios de Aceptacion - -### Escenario 1: Solicitud de recuperacion exitosa - -```gherkin -Given un usuario registrado con email "user@example.com" -When solicita recuperacion de password en POST /api/v1/auth/password/request-reset -Then el sistema responde con status 200 - And el mensaje es "Si el email esta registrado, recibiras instrucciones" - And se genera un token de recuperacion con expiracion de 1 hora - And se envia email con enlace de recuperacion - And tokens anteriores de ese usuario son invalidados -``` - -### Escenario 2: Solicitud para email inexistente (seguridad) - -```gherkin -Given un email "noexiste@example.com" que NO existe en el sistema -When solicita recuperacion de password -Then el sistema responde con status 200 - And el mensaje es IDENTICO al caso exitoso - And NO se revela que el email no existe - And NO se envia ningun email -``` - -### Escenario 3: Cambio de password exitoso - -```gherkin -Given un usuario con token de recuperacion valido - And el token no ha expirado (<1 hora) - And el token no ha sido usado -When envia nuevo password cumpliendo requisitos -Then el sistema actualiza el password hasheado - And invalida el token de recuperacion - And cierra TODAS las sesiones activas del usuario - And envia email confirmando el cambio - And responde con status 200 -``` - -### Escenario 4: Token de recuperacion expirado - -```gherkin -Given un token de recuperacion emitido hace mas de 1 hora -When el usuario intenta usarlo -Then el sistema responde con status 400 - And el mensaje es "Token de recuperacion expirado" -``` - -### Escenario 5: Token ya utilizado - -```gherkin -Given un token de recuperacion que ya fue usado -When el usuario intenta usarlo nuevamente -Then el sistema responde con status 400 - And el mensaje es "Token de recuperacion ya utilizado" -``` - -### Escenario 6: Password no cumple requisitos - -```gherkin -Given un token de recuperacion valido -When el usuario envia un password que no cumple requisitos - | Escenario | Password | Error | - | Muy corto | "abc123" | "Password debe tener minimo 8 caracteres" | - | Sin mayuscula | "password123!" | "Debe incluir una mayuscula" | - | Sin numero | "Password!!" | "Debe incluir un numero" | - | Sin especial | "Password123" | "Debe incluir caracter especial" | -Then el sistema responde con status 400 - And el mensaje indica el requisito faltante - And se incrementa el contador de intentos del token -``` - -### Escenario 7: Password igual a anterior - -```gherkin -Given un token de recuperacion valido - And el usuario tiene historial de passwords -When envia un password igual a uno de los ultimos 5 -Then el sistema responde con status 400 - And el mensaje es "No puedes usar una contraseña anterior" -``` - -### Escenario 8: Validacion de token - -```gherkin -Given un token de recuperacion -When el usuario navega a la pagina de reset con ese token -Then el frontend valida el token GET /api/v1/auth/password/validate-token/:token - And si es valido muestra el formulario - And si no es valido muestra mensaje de error apropiado -``` - ---- - -## Mockup / Wireframe - -### Pagina de Solicitud - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| ╔═══════════════════════════════════╗ | -| ║ RECUPERAR CONTRASEÑA ║ | -| ╚═══════════════════════════════════╝ | -| | -| Ingresa tu correo electronico y te enviaremos | -| instrucciones para restablecer tu contraseña. | -| | -| Correo electronico | -| +---------------------------+ | -| | user@example.com | | -| +---------------------------+ | -| | -| [===== ENVIAR INSTRUCCIONES =====] | -| | -| ← Volver al login | -| | -+------------------------------------------------------------------+ -``` - -### Confirmacion de Envio - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| ╔═══════════════════════════════════╗ | -| ║ EMAIL ENVIADO ✉️ ║ | -| ╚═══════════════════════════════════╝ | -| | -| Si el email esta registrado, recibiras | -| instrucciones en los proximos minutos. | -| | -| Revisa tu bandeja de entrada y la carpeta | -| de spam. | -| | -| [===== VOLVER AL LOGIN =====] | -| | -| ¿No recibiste el email? | -| Espera 5 minutos y vuelve a intentar. | -| | -+------------------------------------------------------------------+ -``` - -### Pagina de Reset - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| ╔═══════════════════════════════════╗ | -| ║ NUEVA CONTRASEÑA ║ | -| ╚═══════════════════════════════════╝ | -| | -| Nueva contraseña | -| +---------------------------+ | -| | •••••••••••• | [👁] | -| +---------------------------+ | -| [████████░░] Fuerte | -| | -| ✓ Minimo 8 caracteres | -| ✓ Al menos una mayuscula | -| ✗ Al menos un numero | -| ✗ Al menos un caracter especial | -| | -| Confirmar contraseña | -| +---------------------------+ | -| | •••••••••••• | [👁] | -| +---------------------------+ | -| | -| [===== CAMBIAR CONTRASEÑA =====] | -| | -+------------------------------------------------------------------+ -``` - -### Token Invalido - -``` -+------------------------------------------------------------------+ -| ERP SUITE | -+------------------------------------------------------------------+ -| | -| ╔═══════════════════════════════════╗ | -| ║ ⚠️ ENLACE INVALIDO ║ | -| ╚═══════════════════════════════════╝ | -| | -| El enlace de recuperacion ha expirado | -| o ya fue utilizado. | -| | -| [===== SOLICITAR NUEVO ENLACE =====] | -| | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API - -```typescript -// 1. Solicitar recuperacion -POST /api/v1/auth/password/request-reset -{ - "email": "user@example.com" -} -// Response 200 -{ - "message": "Si el email esta registrado, recibiras instrucciones" -} - -// 2. Validar token -GET /api/v1/auth/password/validate-token/a1b2c3d4e5f6... -// Response 200 (valido) -{ - "valid": true, - "email": "u***@example.com" -} -// Response 200 (invalido) -{ - "valid": false, - "reason": "expired" | "used" | "invalid" -} - -// 3. Cambiar password -POST /api/v1/auth/password/reset -{ - "token": "a1b2c3d4e5f6...", - "newPassword": "NewSecurePass123!", - "confirmPassword": "NewSecurePass123!" -} -// Response 200 -{ - "message": "Contraseña actualizada exitosamente" -} -``` - -### Template de Email - -```html -Asunto: Recuperacion de contraseña - ERP Suite - -

Hola {{firstName}},

- -

Recibimos una solicitud para restablecer tu contraseña.

- -

Haz clic en el siguiente enlace para crear una nueva contraseña:

- -Restablecer Contraseña - -

Este enlace expira en 1 hora.

- -

Si no solicitaste este cambio, ignora este email. Tu contraseña -permanecera sin cambios.

- -

Por seguridad, nunca compartas este enlace con nadie.

- -
-IP: {{ipAddress}} | Fecha: {{timestamp}} -``` - -### Validaciones de Password - -```typescript -const PASSWORD_RULES = { - minLength: 8, - maxLength: 128, - requireUppercase: true, - requireLowercase: true, - requireNumber: true, - requireSpecial: true, - specialChars: '!@#$%^&*()_+-=[]{}|;:,.<>?', - historyCount: 5, // No repetir ultimos 5 -}; - -const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; -``` - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/auth/password/request-reset implementado -- [ ] Endpoint GET /api/v1/auth/password/validate-token/:token implementado -- [ ] Endpoint POST /api/v1/auth/password/reset implementado -- [ ] Token de 256 bits generado con crypto.randomBytes -- [ ] Token hasheado antes de almacenar -- [ ] Email de recuperacion enviado -- [ ] Validacion de politica de password -- [ ] Historial de passwords implementado -- [ ] Logout-all despues de cambio -- [ ] Email de confirmacion enviado -- [ ] Frontend: ForgotPasswordPage -- [ ] Frontend: ResetPasswordPage -- [ ] Frontend: Password strength indicator -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -### Requiere - -| Item | Descripcion | -|------|-------------| -| EmailService | Para enviar emails | -| Tabla password_reset_tokens | Almacenar tokens | -| Tabla password_history | Historial de passwords | -| US-MGN001-002 | Logout-all functionality | - -### Bloquea - -| Item | Descripcion | -|------|-------------| -| - | No bloquea otras historias | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: requestPasswordReset() | 3h | -| Backend: validateResetToken() | 2h | -| Backend: resetPassword() | 4h | -| Backend: Password validation | 2h | -| Backend: Email templates | 2h | -| Backend: Tests | 3h | -| Frontend: ForgotPasswordPage | 3h | -| Frontend: ResetPasswordPage | 4h | -| Frontend: Password strength | 2h | -| Frontend: Tests | 2h | -| **Total** | **27h** | - ---- - -## Referencias - -- [RF-AUTH-005](../../01-requerimientos/RF-auth/RF-AUTH-005.md) - Requerimiento funcional -- [ET-auth-backend](../../02-modelado/especificaciones-tecnicas/ET-auth-backend.md) - Spec tecnica - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-002/BACKLOG-MGN002.md b/docs/05-user-stories/_legacy_backup/MGN-002/BACKLOG-MGN002.md deleted file mode 100644 index a4e5267..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-002/BACKLOG-MGN002.md +++ /dev/null @@ -1,138 +0,0 @@ -# Backlog del Modulo MGN-002: Users - -## Resumen - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-002 | -| **Nombre** | Users - Gestion de Usuarios | -| **Total User Stories** | 4 | -| **Total Story Points** | 21 | -| **Estado** | En documentacion | -| **Fecha** | 2025-12-05 | - ---- - -## User Stories - -### Sprint 2 - Core Users (16 SP) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| [US-MGN002-001](./US-MGN002-001.md) | CRUD de Usuarios (Admin) | 8 | P0 | Ready | -| [US-MGN002-002](./US-MGN002-002.md) | Perfil de Usuario | 5 | P1 | Ready | -| [US-MGN002-003](./US-MGN002-003.md) | Cambio de Password | 3 | P0 | Ready | - -### Sprint 3 - Personalization (5 SP) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| [US-MGN002-004](./US-MGN002-004.md) | Preferencias de Usuario | 5 | P2 | Ready | - ---- - -## Stories Adicionales (No incluidas en scope inicial) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| US-MGN002-005 | Cambio de Email | 5 | P1 | Backlog | -| US-MGN002-006 | Activacion de Cuenta | 3 | P0 | Backlog | -| US-MGN002-007 | Export de Usuarios (CSV) | 3 | P2 | Backlog | -| US-MGN002-008 | Import de Usuarios (CSV) | 5 | P2 | Backlog | - ---- - -## Roadmap Visual - -``` -Sprint 2 Sprint 3 -├─────────────────────────────────┼─────────────────────────────────┤ -│ US-001: CRUD Usuarios [8 SP] │ US-004: Preferencias [5 SP] │ -│ US-002: Perfil [5 SP] │ │ -│ US-003: Cambio Password [3 SP] │ │ -├─────────────────────────────────┼─────────────────────────────────┤ -│ Total: 16 SP │ Total: 5 SP │ -└─────────────────────────────────┴─────────────────────────────────┘ -``` - ---- - -## Dependencias entre Stories - -``` -RF-AUTH-001 (Login) ─────────────────────────────────────────┐ - │ │ - ▼ │ -US-MGN002-001 (CRUD Admin) ──────────────────────────────────┤ - │ │ - ├──────────────────────────────────────────┐ │ - │ │ │ - ▼ ▼ │ -US-MGN002-002 (Perfil) US-MGN002-003 (Pass) │ - │ │ - └───────────────────────┬──────────────────────────────┘ - │ - ▼ - US-MGN002-004 (Preferencias) -``` - ---- - -## Criterios de Aceptacion del Modulo - -### Funcionalidad - -- [ ] Admins pueden crear, listar, editar y eliminar usuarios -- [ ] Usuarios pueden ver y editar su propio perfil -- [ ] Usuarios pueden cambiar su password -- [ ] Usuarios pueden subir avatar -- [ ] Usuarios pueden configurar preferencias - -### Seguridad - -- [ ] Soft delete en lugar de hard delete -- [ ] Solo admins gestionan otros usuarios -- [ ] Password actual requerido para cambios sensibles -- [ ] Historial de passwords para evitar reuso -- [ ] RBAC en todos los endpoints admin - -### UX - -- [ ] Paginacion y filtros en listados -- [ ] Busqueda por nombre y email -- [ ] Avatar con resize automatico -- [ ] Preferencias aplicadas inmediatamente - ---- - -## Estimacion Total - -| Capa | Story Points | -|------|--------------| -| Database | 6 | -| Backend | 15 | -| Frontend | 16 | -| **Total** | **37** | - -> Nota: Esta estimacion corresponde a los 5 RFs completos, no solo las 4 US principales. - ---- - -## Definition of Done del Modulo - -- [ ] Todas las User Stories completadas -- [ ] Tests unitarios > 80% coverage -- [ ] Tests e2e pasando -- [ ] Documentacion Swagger completa -- [ ] Code review aprobado -- [ ] Security review aprobado -- [ ] Despliegue en staging exitoso -- [ ] UAT aprobado - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 US | diff --git a/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-001.md b/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-001.md deleted file mode 100644 index b446f8b..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-001.md +++ /dev/null @@ -1,219 +0,0 @@ -# US-MGN002-001: CRUD de Usuarios (Admin) - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-001 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** poder crear, ver, editar y eliminar usuarios -**Para** gestionar el acceso de los empleados a la plataforma ERP - ---- - -## Criterios de Aceptacion - -### Escenario 1: Crear usuario exitosamente - -```gherkin -Given un administrador autenticado con permiso "users:create" -When crea un usuario con: - | email | nuevo@empresa.com | - | firstName | Juan | - | lastName | Perez | - | roleIds | [admin-role-id] | -Then el sistema crea el usuario con status "pending_activation" - And envia email de invitacion al usuario - And responde con status 201 - And el body contiene el usuario creado sin password -``` - -### Escenario 2: Listar usuarios con paginacion - -```gherkin -Given 50 usuarios en el tenant actual -When el admin solicita GET /api/v1/users?page=2&limit=10 -Then el sistema retorna usuarios 11-20 - And incluye meta con total, pages, hasNext, hasPrev -``` - -### Escenario 3: Buscar usuarios por texto - -```gherkin -Given usuarios con nombres "Juan", "Juana", "Pedro" -When el admin busca con search="juan" -Then el sistema retorna "Juan" y "Juana" - And no retorna "Pedro" -``` - -### Escenario 4: Soft delete de usuario - -```gherkin -Given un usuario activo con id "user-123" -When el admin elimina el usuario -Then el campo deleted_at se establece - And el campo deleted_by tiene el ID del admin - And el usuario no aparece en listados - And el usuario no puede hacer login -``` - -### Escenario 5: No puede eliminarse a si mismo - -```gherkin -Given un admin con id "admin-123" -When intenta eliminar su propio usuario -Then el sistema responde con status 400 - And el mensaje es "No puedes eliminarte a ti mismo" -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Usuarios [+ Nuevo Usuario] | -+------------------------------------------------------------------+ -| Buscar: [___________________] [Filtros ▼] | -+------------------------------------------------------------------+ -| ☐ | Avatar | Nombre | Email | Estado | Roles | -|---|--------|---------------|--------------------|---------|----- | -| ☐ | 👤 | Juan Perez | juan@empresa.com | Activo | Admin| -| ☐ | 👤 | Maria Lopez | maria@empresa.com | Activo | User | -| ☐ | 👤 | Pedro Garcia | pedro@empresa.com | Inactivo| User | -+------------------------------------------------------------------+ -| Mostrando 1-10 de 50 [< Anterior] [Siguiente >]| -+------------------------------------------------------------------+ - -Modal: Crear Usuario -┌──────────────────────────────────────────────────────────────────┐ -│ NUEVO USUARIO │ -├──────────────────────────────────────────────────────────────────┤ -│ Email* [_______________________________] │ -│ Nombre* [_______________________________] │ -│ Apellido* [_______________________________] │ -│ Telefono [_______________________________] │ -│ Roles [Select roles... ▼] │ -│ ☑ Admin ☐ Manager ☐ User │ -│ │ -│ [ Cancelar ] [ Crear Usuario ] │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Crear usuario -POST /api/v1/users -{ - "email": "nuevo@empresa.com", - "firstName": "Juan", - "lastName": "Perez", - "phone": "+521234567890", - "roleIds": ["role-uuid"] -} - -// Response 201 -{ - "id": "user-uuid", - "email": "nuevo@empresa.com", - "firstName": "Juan", - "lastName": "Perez", - "status": "pending_activation", - "isActive": false, - "createdAt": "2025-12-05T10:00:00Z", - "roles": [{ "id": "role-uuid", "name": "admin" }] -} - -// Listar usuarios -GET /api/v1/users?page=1&limit=20&search=juan&status=active&sortBy=createdAt&sortOrder=DESC - -// Response 200 -{ - "data": [...], - "meta": { - "total": 50, - "page": 1, - "limit": 20, - "totalPages": 3, - "hasNext": true, - "hasPrev": false - } -} -``` - -### Permisos Requeridos - -| Accion | Permiso | -|--------|---------| -| Crear | users:create | -| Listar | users:read | -| Ver detalle | users:read | -| Actualizar | users:update | -| Eliminar | users:delete | -| Activar/Desactivar | users:update | - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/users implementado -- [ ] Endpoint GET /api/v1/users con paginacion y filtros -- [ ] Endpoint GET /api/v1/users/:id -- [ ] Endpoint PATCH /api/v1/users/:id -- [ ] Endpoint DELETE /api/v1/users/:id (soft delete) -- [ ] Validaciones de permisos (RBAC) -- [ ] Email de invitacion enviado -- [ ] Frontend: UsersListPage con tabla -- [ ] Frontend: Modal crear/editar usuario -- [ ] Tests unitarios (>80% coverage) -- [ ] Tests e2e pasando -- [ ] Code review aprobado - ---- - -## Dependencias - -| ID | Descripcion | -|----|-------------| -| RF-AUTH-001 | Login para autenticacion | -| RF-ROLE-001 | Roles para asignacion | -| EmailService | Para enviar invitaciones | - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: CRUD endpoints | 6h | -| Backend: Paginacion y filtros | 3h | -| Backend: Tests | 3h | -| Frontend: UsersListPage | 4h | -| Frontend: UserForm modal | 3h | -| Frontend: Tests | 2h | -| **Total** | **21h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-002.md b/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-002.md deleted file mode 100644 index d880c16..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-002.md +++ /dev/null @@ -1,225 +0,0 @@ -# US-MGN002-002: Perfil de Usuario - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-002 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 2 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 5 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema -**Quiero** poder ver y editar mi informacion personal -**Para** mantener mis datos actualizados y personalizar mi experiencia - ---- - -## Criterios de Aceptacion - -### Escenario 1: Ver mi perfil - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/users/me -Then el sistema retorna su perfil completo - And incluye firstName, lastName, email, phone - And incluye avatarUrl, avatarThumbnailUrl - And incluye createdAt, lastLoginAt - And incluye sus roles asignados - And NO incluye passwordHash -``` - -### Escenario 2: Actualizar nombre - -```gherkin -Given un usuario autenticado con nombre "Juan" -When actualiza su nombre a "Carlos" -Then el sistema guarda el cambio - And responde con el perfil actualizado - And firstName es "Carlos" -``` - -### Escenario 3: Subir avatar - -```gherkin -Given un usuario autenticado - And una imagen JPG de 2MB -When sube la imagen como avatar -Then el sistema redimensiona a 200x200 px - And genera thumbnail de 50x50 px - And actualiza avatarUrl en su perfil - And responde con las URLs generadas -``` - -### Escenario 4: Avatar muy grande - -```gherkin -Given un usuario autenticado - And una imagen de 15MB -When intenta subir como avatar -Then el sistema responde con status 400 - And el mensaje es "Imagen excede tamaño maximo (10MB)" -``` - -### Escenario 5: Eliminar avatar - -```gherkin -Given un usuario con avatar -When elimina su avatar -Then avatarUrl se establece a null - And avatarThumbnailUrl se establece a null - And el avatar anterior se marca como no actual -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Mi Perfil | -+------------------------------------------------------------------+ -| | -| +-------+ | -| | | Juan Perez | -| | FOTO | juan@empresa.com | -| | | Admin, Manager | -| +-------+ | -| [Cambiar foto] | -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ INFORMACION PERSONAL │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Nombre [Juan ] │ | -| │ Apellido [Perez ] │ | -| │ Telefono [+521234567890 ] │ | -| │ Email juan@empresa.com [Cambiar email] │ | -| │ │ | -| │ [ Cancelar ] [ Guardar Cambios ] │ | -| └─────────────────────────────────────────────────────────┘ | -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ SEGURIDAD │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Contraseña •••••••• [Cambiar contraseña] │ | -| │ Ultimo login 05/12/2025 10:30 │ | -| │ Miembro desde 01/01/2025 │ | -| └─────────────────────────────────────────────────────────┘ | -+------------------------------------------------------------------+ - -Modal: Subir Avatar -┌──────────────────────────────────────────────────────────────────┐ -│ CAMBIAR FOTO │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ +------------------+ │ -│ | | Arrastra una imagen o │ -│ | [ + ] | [Selecciona archivo] │ -│ | | │ -│ +------------------+ Formatos: JPG, PNG, WebP │ -│ Tamaño max: 10MB │ -│ │ -│ [ Cancelar ] [ Subir ] │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Obtener perfil -GET /api/v1/users/me - -// Response 200 -{ - "id": "user-uuid", - "email": "juan@empresa.com", - "firstName": "Juan", - "lastName": "Perez", - "phone": "+521234567890", - "avatarUrl": "https://storage.../avatar-200.jpg", - "avatarThumbnailUrl": "https://storage.../avatar-50.jpg", - "status": "active", - "emailVerifiedAt": "2025-01-01T00:00:00Z", - "lastLoginAt": "2025-12-05T10:30:00Z", - "createdAt": "2025-01-01T00:00:00Z", - "roles": [{ "id": "...", "name": "admin" }] -} - -// Actualizar perfil -PATCH /api/v1/users/me -{ - "firstName": "Carlos", - "lastName": "Lopez", - "phone": "+521234567890" -} - -// Subir avatar -POST /api/v1/users/me/avatar -Content-Type: multipart/form-data -avatar: [file] - -// Response 200 -{ - "avatarUrl": "https://storage.../avatar-200.jpg", - "avatarThumbnailUrl": "https://storage.../avatar-50.jpg" -} -``` - -### Validaciones de Avatar - -| Validacion | Valor | -|------------|-------| -| Formatos | image/jpeg, image/png, image/webp | -| Tamaño max | 10MB | -| Resize main | 200x200 px | -| Resize thumb | 50x50 px | - ---- - -## Definicion de Done - -- [ ] Endpoint GET /api/v1/users/me -- [ ] Endpoint PATCH /api/v1/users/me -- [ ] Endpoint POST /api/v1/users/me/avatar -- [ ] Endpoint DELETE /api/v1/users/me/avatar -- [ ] Procesamiento de imagen con Sharp -- [ ] Upload a storage (S3/local) -- [ ] Frontend: ProfilePage -- [ ] Frontend: AvatarUploader component -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Profile endpoints | 3h | -| Backend: Avatar upload + resize | 4h | -| Backend: Tests | 2h | -| Frontend: ProfilePage | 4h | -| Frontend: AvatarUploader | 3h | -| Frontend: Tests | 2h | -| **Total** | **18h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-003.md b/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-003.md deleted file mode 100644 index 5ce1d05..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-003.md +++ /dev/null @@ -1,203 +0,0 @@ -# US-MGN002-003: Cambio de Password - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-003 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 3 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema -**Quiero** poder cambiar mi contraseña -**Para** mantener mi cuenta segura y cumplir con politicas de rotacion - ---- - -## Criterios de Aceptacion - -### Escenario 1: Cambio exitoso - -```gherkin -Given un usuario autenticado -When proporciona password actual correcto "OldPass123!" - And nuevo password "NewPass456!" cumple requisitos -Then el sistema actualiza el password - And guarda hash en password_history - And envia email de notificacion - And responde con status 200 -``` - -### Escenario 2: Password actual incorrecto - -```gherkin -Given un usuario autenticado -When proporciona password actual incorrecto -Then el sistema responde con status 400 - And el mensaje es "Password actual incorrecto" -``` - -### Escenario 3: Nuevo password no cumple requisitos - -```gherkin -Given un usuario autenticado -When el nuevo password es "abc123" -Then el sistema responde con status 400 - And lista los requisitos no cumplidos: - | Debe tener al menos 8 caracteres | - | Debe incluir una mayuscula | - | Debe incluir caracter especial | -``` - -### Escenario 4: Password reutilizado - -```gherkin -Given un usuario que uso "MiPass123!" hace 2 meses -When intenta cambiar a "MiPass123!" -Then el sistema responde con status 400 - And el mensaje es "No puedes usar un password que hayas usado anteriormente" -``` - -### Escenario 5: Cerrar otras sesiones - -```gherkin -Given un usuario con 3 sesiones activas -When cambia password con logoutOtherSessions=true -Then el sistema actualiza el password - And invalida las otras 2 sesiones - And la sesion actual permanece activa - And responde con sessionsInvalidated: 2 -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Cambiar Contraseña | -+------------------------------------------------------------------+ -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ CAMBIAR CONTRASEÑA │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ │ | -| │ Contraseña actual │ | -| │ [••••••••••••••••••• ] 👁 │ | -| │ │ | -| │ Nueva contraseña │ | -| │ [••••••••••••••••••• ] 👁 │ | -| │ [████████████░░░░░░░░] Fuerte │ | -| │ │ | -| │ ✓ Minimo 8 caracteres │ | -| │ ✓ Al menos una mayuscula │ | -| │ ✓ Al menos una minuscula │ | -| │ ✗ Al menos un numero │ | -| │ ✗ Al menos un caracter especial │ | -| │ │ | -| │ Confirmar nueva contraseña │ | -| │ [••••••••••••••••••• ] 👁 │ | -| │ │ | -| │ ☐ Cerrar sesion en otros dispositivos │ | -| │ │ | -| │ [ Cancelar ] [ Cambiar Contraseña ] │ | -| └─────────────────────────────────────────────────────────┘ | -| | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoint - -```typescript -POST /api/v1/users/me/password -{ - "currentPassword": "OldPass123!", - "newPassword": "NewPass456!", - "confirmPassword": "NewPass456!", - "logoutOtherSessions": true -} - -// Response 200 -{ - "message": "Password actualizado exitosamente", - "sessionsInvalidated": 2 -} - -// Response 400 - Password incorrecto -{ - "statusCode": 400, - "message": "Password actual incorrecto" -} - -// Response 400 - No cumple politica -{ - "statusCode": 400, - "message": "El password no cumple los requisitos", - "errors": [ - "Debe incluir al menos una mayuscula", - "Debe incluir al menos un caracter especial" - ] -} -``` - -### Politica de Password - -``` -- Minimo 8 caracteres -- Maximo 128 caracteres -- Al menos 1 mayuscula -- Al menos 1 minuscula -- Al menos 1 numero -- Al menos 1 caracter especial (!@#$%^&*) -- No puede contener el email -- No puede ser igual a los ultimos 5 -``` - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/users/me/password -- [ ] Validacion de password actual -- [ ] Validacion de politica de complejidad -- [ ] Verificacion contra historial (5 anteriores) -- [ ] Opcion de cerrar otras sesiones -- [ ] Email de notificacion enviado -- [ ] Frontend: ChangePasswordForm -- [ ] Frontend: PasswordStrengthIndicator -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Endpoint | 2h | -| Backend: Validaciones | 2h | -| Backend: Tests | 1h | -| Frontend: Form + indicator | 3h | -| Frontend: Tests | 1h | -| **Total** | **9h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-004.md b/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-004.md deleted file mode 100644 index 3408e03..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-002/US-MGN002-004.md +++ /dev/null @@ -1,222 +0,0 @@ -# US-MGN002-004: Preferencias de Usuario - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN002-004 | -| **Modulo** | MGN-002 Users | -| **Sprint** | Sprint 3 | -| **Prioridad** | P2 - Media | -| **Story Points** | 5 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** usuario autenticado del sistema -**Quiero** poder configurar mis preferencias personales -**Para** personalizar mi experiencia en la plataforma - ---- - -## Criterios de Aceptacion - -### Escenario 1: Obtener preferencias - -```gherkin -Given un usuario autenticado -When accede a GET /api/v1/users/me/preferences -Then el sistema retorna sus preferencias - And si no tiene preferencias, retorna defaults del tenant -``` - -### Escenario 2: Cambiar idioma - -```gherkin -Given un usuario con idioma "es" -When cambia a idioma "en" -Then el sistema guarda la preferencia - And la interfaz cambia a ingles inmediatamente -``` - -### Escenario 3: Cambiar tema - -```gherkin -Given un usuario con tema "light" -When activa tema "dark" -Then la interfaz cambia a colores oscuros - And la preferencia persiste entre sesiones -``` - -### Escenario 4: Configurar notificaciones - -```gherkin -Given un usuario con notificaciones de marketing activas -When desactiva notificaciones de marketing -Then deja de recibir ese tipo de emails - And mantiene otras notificaciones activas -``` - -### Escenario 5: Reset preferencias - -```gherkin -Given un usuario con preferencias personalizadas -When ejecuta reset de preferencias -Then todas sus preferencias vuelven a defaults del tenant -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Preferencias | -+------------------------------------------------------------------+ -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ IDIOMA Y REGION │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Idioma [Español ▼] │ | -| │ Zona horaria [America/Mexico_City ▼] │ | -| │ Formato fecha [DD/MM/YYYY ▼] │ | -| │ Formato hora [24 horas ▼] │ | -| │ Moneda [MXN - Peso Mexicano ▼] │ | -| └─────────────────────────────────────────────────────────┘ | -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ APARIENCIA │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Tema [☀️ Claro] [🌙 Oscuro] [💻 Sistema] │ | -| │ Tamaño fuente [Pequeño] [Mediano] [Grande] │ | -| │ ☐ Modo compacto │ | -| │ ☐ Sidebar colapsado por defecto │ | -| └─────────────────────────────────────────────────────────┘ | -| | -| ┌─────────────────────────────────────────────────────────┐ | -| │ NOTIFICACIONES │ | -| ├─────────────────────────────────────────────────────────┤ | -| │ Email │ | -| │ ☑ Habilitado Frecuencia: [Diario ▼] │ | -| │ ☑ Alertas de seguridad │ | -| │ ☑ Actualizaciones del sistema │ | -| │ ☐ Comunicaciones de marketing │ | -| │ │ | -| │ Push │ | -| │ ☑ Habilitado │ | -| │ ☑ Sonido de notificacion │ | -| │ │ | -| │ In-App │ | -| │ ☑ Habilitado │ | -| │ ☐ Notificaciones de escritorio │ | -| └─────────────────────────────────────────────────────────┘ | -| | -| [Restaurar valores predeterminados] [ Guardar Cambios ] | -+------------------------------------------------------------------+ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Obtener preferencias -GET /api/v1/users/me/preferences - -// Response 200 -{ - "language": "es", - "timezone": "America/Mexico_City", - "dateFormat": "DD/MM/YYYY", - "timeFormat": "24h", - "currency": "MXN", - "numberFormat": "es-MX", - "theme": "dark", - "sidebarCollapsed": false, - "compactMode": false, - "fontSize": "medium", - "notifications": { - "email": { - "enabled": true, - "digest": "daily", - "marketing": false, - "security": true, - "updates": true - }, - "push": { "enabled": true, "sound": true }, - "inApp": { "enabled": true, "desktop": false } - }, - "dashboard": { - "defaultView": "overview", - "widgets": ["sales", "inventory"] - } -} - -// Actualizar preferencias (parcial) -PATCH /api/v1/users/me/preferences -{ - "theme": "light", - "notifications": { - "email": { "marketing": false } - } -} - -// Reset preferencias -POST /api/v1/users/me/preferences/reset -``` - -### Aplicacion en Frontend - -```typescript -// Al cambiar tema -useEffect(() => { - document.documentElement.setAttribute('data-theme', preferences.theme); -}, [preferences.theme]); - -// Al cambiar idioma -useEffect(() => { - i18n.changeLanguage(preferences.language); -}, [preferences.language]); -``` - ---- - -## Definicion de Done - -- [ ] Endpoint GET /api/v1/users/me/preferences -- [ ] Endpoint PATCH /api/v1/users/me/preferences -- [ ] Endpoint POST /api/v1/users/me/preferences/reset -- [ ] Deep merge para actualizaciones parciales -- [ ] Frontend: PreferencesPage con todas las secciones -- [ ] Frontend: Aplicacion inmediata de cambios (tema, idioma) -- [ ] Frontend: PreferencesContext para estado global -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Endpoints | 3h | -| Backend: Deep merge logic | 1h | -| Backend: Tests | 2h | -| Frontend: PreferencesPage | 5h | -| Frontend: Theme/Language context | 3h | -| Frontend: Tests | 2h | -| **Total** | **16h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-003/BACKLOG-MGN003.md b/docs/05-user-stories/_legacy_backup/MGN-003/BACKLOG-MGN003.md deleted file mode 100644 index 09b5bb1..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-003/BACKLOG-MGN003.md +++ /dev/null @@ -1,177 +0,0 @@ -# Backlog del Modulo MGN-003: Roles/RBAC - -## Resumen - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-003 | -| **Nombre** | Roles/RBAC - Control de Acceso | -| **Total User Stories** | 4 | -| **Total Story Points** | 29 | -| **Estado** | En documentacion | -| **Fecha** | 2025-12-05 | - ---- - -## User Stories - -### Sprint 2 - Core RBAC (29 SP) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| [US-MGN003-001](./US-MGN003-001.md) | CRUD de Roles | 8 | P0 | Ready | -| [US-MGN003-002](./US-MGN003-002.md) | Gestion de Permisos | 5 | P0 | Ready | -| [US-MGN003-003](./US-MGN003-003.md) | Asignacion Roles-Usuarios | 8 | P0 | Ready | -| [US-MGN003-004](./US-MGN003-004.md) | Control de Acceso RBAC | 8 | P0 | Ready | - ---- - -## Stories Adicionales (No incluidas en scope inicial) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| US-MGN003-005 | Roles Temporales | 3 | P2 | Backlog | -| US-MGN003-006 | Herencia de Roles | 5 | P2 | Backlog | -| US-MGN003-007 | Auditoria de Cambios RBAC | 3 | P1 | Backlog | -| US-MGN003-008 | Permisos Contextuales | 5 | P2 | Backlog | - ---- - -## Roadmap Visual - -``` -Sprint 2 -├─────────────────────────────────────────────────────────────────┤ -│ US-001: CRUD de Roles [8 SP] │ -│ US-002: Gestion de Permisos [5 SP] │ -│ US-003: Asignacion Roles-Usuarios [8 SP] │ -│ US-004: Control de Acceso RBAC [8 SP] │ -├─────────────────────────────────────────────────────────────────┤ -│ Total: 29 SP │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Dependencias entre Stories - -``` - ┌─────────────────┐ - │ MGN-001 Auth │ - │ (JWT/Login) │ - └────────┬────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ US-MGN003-002 │ - │ Gestion de Permisos │ - │ (Catalogo de permisos) │ - └────────────┬─────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ US-MGN003-001 │ - │ CRUD de Roles │ - │ (Roles con permisos) │ - └────────────┬─────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ US-MGN003-003 │ - │ Asignacion Roles │ - │ (Usuarios con roles) │ - └────────────┬─────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ US-MGN003-004 │ - │ Control de Acceso │ - │ (Guards y decoradores) │ - └────────────┬─────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Todos los endpoints │ - │ del sistema usan RBAC │ - └──────────────────────────┘ -``` - ---- - -## Criterios de Aceptacion del Modulo - -### Funcionalidad - -- [ ] Admins pueden crear, listar, editar y eliminar roles personalizados -- [ ] Roles built-in no pueden eliminarse ni modificarse (solo extender) -- [ ] Permisos se muestran agrupados por modulo -- [ ] Usuarios pueden tener multiples roles -- [ ] Permisos efectivos son la union de todos los roles -- [ ] Wildcards funcionan correctamente (users:* -> users:read, etc.) -- [ ] Control de acceso en TODOS los endpoints protegidos - -### Seguridad - -- [ ] Solo super_admin puede asignar rol super_admin -- [ ] Al menos un super_admin debe existir siempre -- [ ] Errores 403 no revelan que permiso falta -- [ ] Logs de acceso denegado para auditoria -- [ ] Cache de permisos se invalida al cambiar roles - -### Performance - -- [ ] Permisos cacheados por 5 minutos -- [ ] Validacion de permisos < 10ms (desde cache) -- [ ] Invalidacion de cache inmediata - ---- - -## Estimacion Total - -| Capa | Story Points | -|------|--------------| -| Backend: Endpoints | 10 | -| Backend: Guards/Decorators | 8 | -| Backend: Cache | 4 | -| Backend: Tests | 7 | -| Frontend: Pages | 8 | -| Frontend: Components | 6 | -| Frontend: Tests | 5 | -| **Total** | **48** | - -> Nota: Las 4 US principales suman 29 SP. La estimacion detallada de 48 SP incluye integracion y overhead. - ---- - -## Definition of Done del Modulo - -- [ ] Todas las User Stories completadas -- [ ] Permisos seeded en base de datos -- [ ] Roles built-in creados para cada tenant -- [ ] Guards aplicados a todos los endpoints existentes -- [ ] Tests unitarios > 80% coverage -- [ ] Tests e2e de flujos RBAC -- [ ] Documentacion Swagger completa -- [ ] Code review aprobado -- [ ] Security review aprobado -- [ ] Despliegue en staging exitoso -- [ ] UAT aprobado - ---- - -## Riesgos y Mitigaciones - -| Riesgo | Impacto | Probabilidad | Mitigacion | -|--------|---------|--------------|------------| -| Performance de validacion | Alto | Media | Cache de permisos | -| Cache desincronizado | Alto | Baja | Invalidacion inmediata | -| Configuracion incorrecta | Medio | Media | Tests exhaustivos | -| Bloqueo de usuarios | Alto | Baja | Super Admin bypass | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 US | diff --git a/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-001.md b/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-001.md deleted file mode 100644 index 72cbe99..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-001.md +++ /dev/null @@ -1,162 +0,0 @@ -# US-MGN003-001: CRUD de Roles - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN003-001 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** poder crear, ver, editar y eliminar roles -**Para** gestionar los niveles de acceso de los usuarios a las funcionalidades del sistema - ---- - -## Criterios de Aceptacion - -### Escenario 1: Crear rol exitosamente - -```gherkin -Given un admin autenticado con permiso "roles:create" -When crea un rol con: - | name | Vendedor | - | description | Equipo de ventas | - | permissions | [sales:*, users:read] | -Then el sistema crea el rol - And el rol tiene slug "vendedor" - And el rol tiene isBuiltIn = false - And responde con status 201 -``` - -### Escenario 2: Listar roles con paginacion - -```gherkin -Given 12 roles en el tenant (5 built-in + 7 custom) -When el admin solicita GET /api/v1/roles?page=1&limit=10 -Then el sistema retorna 10 roles - And roles built-in aparecen primero - And incluye usersCount y permissionsCount por rol - And incluye meta con total=12 -``` - -### Escenario 3: No crear rol con nombre duplicado - -```gherkin -Given un rol existente con nombre "Vendedor" -When el admin intenta crear otro rol "Vendedor" -Then el sistema responde con status 409 - And el mensaje es "Ya existe un rol con este nombre" -``` - -### Escenario 4: No eliminar rol del sistema - -```gherkin -Given el rol built-in "admin" -When el admin intenta eliminarlo -Then el sistema responde con status 400 - And el mensaje es "No se pueden eliminar roles del sistema" -``` - -### Escenario 5: Soft delete con reasignacion - -```gherkin -Given rol "Vendedor" con 5 usuarios asignados -When admin elimina el rol con reassignTo="user" -Then el rol tiene deleted_at establecido - And los 5 usuarios ahora tienen rol "user" - And el rol no aparece en listados normales -``` - ---- - -## Mockup / Wireframe - -``` -+------------------------------------------------------------------+ -| [Logo] Roles [+ Nuevo Rol] | -+------------------------------------------------------------------+ -| Buscar: [___________________] [Tipo: Todos ▼] | -+------------------------------------------------------------------+ -| | Nombre | Descripcion | Permisos | Usuarios | ⚙ | -|---|-----------------|--------------------|-----------|---------|----| -| 🔒| Super Admin | Acceso total | 45 | 1 | 👁 | -| 🔒| Admin | Gestion tenant | 32 | 3 | 👁 | -| 🔒| Manager | Supervision | 18 | 5 | 👁 | -| 🔒| User | Acceso basico | 8 | 42 | 👁 | -| | Vendedor | Equipo de ventas | 12 | 8 | ✏🗑| -| | Contador | Area contable | 15 | 2 | ✏🗑| -+------------------------------------------------------------------+ - -🔒 = Rol del sistema (built-in), no editable/eliminable -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -POST /api/v1/roles -GET /api/v1/roles?page=1&limit=10&type=custom&search=vend -GET /api/v1/roles/:id -PATCH /api/v1/roles/:id -DELETE /api/v1/roles/:id?reassignTo=other-role-id -``` - -### Validaciones - -| Campo | Regla | -|-------|-------| -| name | 3-50 chars, unico en tenant | -| description | Max 500 chars | -| permissionIds | Array min 1 | - ---- - -## Definicion de Done - -- [ ] Endpoint POST /api/v1/roles -- [ ] Endpoint GET /api/v1/roles con paginacion -- [ ] Endpoint GET /api/v1/roles/:id -- [ ] Endpoint PATCH /api/v1/roles/:id -- [ ] Endpoint DELETE /api/v1/roles/:id -- [ ] Validacion de roles built-in -- [ ] Frontend: RolesListPage -- [ ] Frontend: CreateRoleModal -- [ ] Frontend: EditRoleModal -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: CRUD endpoints | 5h | -| Backend: Validaciones | 2h | -| Backend: Tests | 2h | -| Frontend: RolesListPage | 4h | -| Frontend: RoleForm | 4h | -| Frontend: Tests | 2h | -| **Total** | **19h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-002.md b/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-002.md deleted file mode 100644 index aaf1b44..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-002.md +++ /dev/null @@ -1,177 +0,0 @@ -# US-MGN003-002: Gestion de Permisos - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN003-002 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 5 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** ver el catalogo de permisos disponibles agrupados por modulo -**Para** asignar los permisos correctos a cada rol - ---- - -## Criterios de Aceptacion - -### Escenario 1: Listar permisos agrupados - -```gherkin -Given un admin autenticado con permiso "permissions:read" -When accede a GET /api/v1/permissions -Then el sistema retorna permisos agrupados por modulo - And cada modulo incluye nombre y lista de permisos - And cada permiso incluye code, name, description - And no incluye permisos deprecados -``` - -### Escenario 2: Buscar permisos - -```gherkin -Given 40 permisos en el sistema -When el admin busca "users" -Then el sistema retorna solo permisos que contienen "users" - And mantiene agrupacion por modulo - And la busqueda es case-insensitive -``` - -### Escenario 3: Mostrar permisos en selector - -```gherkin -Given admin creando un nuevo rol -When ve el selector de permisos -Then los permisos estan agrupados por modulo - And puede expandir/colapsar cada modulo - And puede seleccionar multiples permisos - And puede seleccionar "todos" de un modulo -``` - -### Escenario 4: Wildcard en permisos - -```gherkin -Given un rol con permiso "users:*" -When se visualizan los permisos del rol -Then aparece "users:*" como seleccionado - And indica que incluye todos los permisos de usuarios - And los permisos individuales aparecen como "incluidos" -``` - ---- - -## Mockup / Wireframe - -``` -Selector de Permisos (en modal de crear/editar rol) -┌────────────────────────────────────────────────────────────────┐ -│ Buscar permisos: [________________] [Seleccionar todos] │ -├────────────────────────────────────────────────────────────────┤ -│ │ -│ ▼ Gestion de Usuarios (7 permisos) [☑ Todos] │ -│ ┌──────────────────────────────────────────────────────────┐│ -│ │ ☑ users:read - Leer usuarios ││ -│ │ ☑ users:create - Crear usuarios ││ -│ │ ☐ users:update - Actualizar usuarios ││ -│ │ ☐ users:delete - Eliminar usuarios ││ -│ │ ☐ users:activate - Activar/Desactivar usuarios ││ -│ │ ☐ users:export - Exportar lista de usuarios ││ -│ │ ☐ users:import - Importar usuarios ││ -│ └──────────────────────────────────────────────────────────┘│ -│ │ -│ ▶ Roles y Permisos (6 permisos) [☐ Todos] │ -│ │ -│ ▶ Inventario (9 permisos) [☐ Todos] │ -│ │ -│ ▶ Modulo Financiero (7 permisos) [☐ Todos] │ -│ │ -├────────────────────────────────────────────────────────────────┤ -│ Permisos seleccionados: 2 │ -└────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -GET /api/v1/permissions?search=users - -// Response -{ - "data": [ - { - "module": "users", - "moduleName": "Gestion de Usuarios", - "permissions": [ - { - "id": "uuid", - "code": "users:read", - "name": "Leer usuarios", - "description": "Ver listado y detalle de usuarios" - }, - // ... - ] - } - ] -} -``` - -### Permisos por Modulo - -| Modulo | Cantidad | Ejemplo | -|--------|----------|---------| -| auth | 2 | auth:sessions:read | -| users | 7 | users:read, users:create | -| roles | 6 | roles:read, roles:assign | -| tenants | 3 | tenants:read | -| settings | 2 | settings:update | -| audit | 2 | audit:read | -| reports | 4 | reports:export | -| financial | 7 | financial:transactions:create | -| inventory | 9 | inventory:products:read | - ---- - -## Definicion de Done - -- [ ] Endpoint GET /api/v1/permissions -- [ ] Busqueda por texto -- [ ] Agrupacion por modulo -- [ ] Frontend: PermissionSelector component -- [ ] Frontend: Expand/collapse por modulo -- [ ] Frontend: Seleccion multiple -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Endpoint | 2h | -| Backend: Agrupacion | 1h | -| Backend: Tests | 1h | -| Frontend: PermissionSelector | 5h | -| Frontend: Tests | 2h | -| **Total** | **11h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-003.md b/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-003.md deleted file mode 100644 index 6c1fdcf..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-003.md +++ /dev/null @@ -1,211 +0,0 @@ -# US-MGN003-003: Asignacion de Roles a Usuarios - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN003-003 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** administrador del sistema -**Quiero** poder asignar y quitar roles a los usuarios -**Para** controlar que acciones puede realizar cada usuario en el sistema - ---- - -## Criterios de Aceptacion - -### Escenario 1: Asignar rol a usuario - -```gherkin -Given un usuario "juan@empresa.com" con rol "user" - And un rol "vendedor" disponible -When el admin asigna rol "vendedor" al usuario -Then el usuario tiene roles ["user", "vendedor"] - And el usuario tiene permisos de ambos roles combinados - And se registra en auditoria quien hizo la asignacion -``` - -### Escenario 2: Quitar rol de usuario - -```gherkin -Given un usuario con roles ["admin", "manager"] -When el admin quita el rol "manager" -Then el usuario solo tiene rol ["admin"] - And pierde los permisos exclusivos de "manager" - And los cambios son inmediatos -``` - -### Escenario 3: Asignacion masiva - -```gherkin -Given 10 usuarios seleccionados - And rol "vendedor" disponible -When el admin asigna "vendedor" a todos -Then los 10 usuarios tienen rol "vendedor" agregado - And responde con resumen: { success: 10, failed: 0 } -``` - -### Escenario 4: Proteccion de super_admin - -```gherkin -Given un admin autenticado (no super_admin) -When intenta asignar rol "super_admin" a un usuario -Then el sistema responde con status 403 - And el mensaje es "Solo Super Admin puede asignar este rol" -``` - -### Escenario 5: No quitar ultimo super_admin - -```gherkin -Given solo 1 usuario con rol "super_admin" -When se intenta quitar el rol -Then el sistema responde con status 400 - And el mensaje es "Debe existir al menos un Super Admin" -``` - -### Escenario 6: Ver permisos efectivos - -```gherkin -Given usuario con roles ["admin", "vendedor"] -When consulta GET /api/v1/users/:id/permissions -Then retorna union de permisos de ambos roles - And indica cuales son directos y cuales heredados -``` - ---- - -## Mockup / Wireframe - -``` -Modal: Gestionar Roles de Usuario -┌──────────────────────────────────────────────────────────────────┐ -│ ROLES DE: Juan Perez (juan@empresa.com) │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ Selecciona los roles para este usuario: │ -│ │ -│ ┌────────────────────────────────────────────────────────────┐ │ -│ │ 🔒 Super Admin Acceso total al sistema [ ] │ │ -│ │ 🔒 Admin Gestion completa del tenant [✓] │ │ -│ │ 🔒 Manager Supervision operativa [ ] │ │ -│ │ 🔒 User Acceso basico [✓] │ │ -│ │ Vendedor Equipo de ventas [✓] │ │ -│ │ Contador Area contable [ ] │ │ -│ │ Almacenista Gestion de inventario [ ] │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ Roles seleccionados: 3 (Admin, User, Vendedor) │ -│ Permisos efectivos: 45 permisos │ -│ │ -│ [Ver detalle de permisos] │ -│ │ -│ [ Cancelar ] [ Guardar Cambios ] │ -└──────────────────────────────────────────────────────────────────┘ - -Vista: Usuarios de un Rol (desde detalle de rol) -┌──────────────────────────────────────────────────────────────────┐ -│ Rol: Vendedor [+ Agregar Usuarios]│ -├──────────────────────────────────────────────────────────────────┤ -│ Usuarios con este rol (8): │ -│ │ -│ | Avatar | Nombre | Email | Desde | ⚙ | -│ |--------|----------------|--------------------|-----------|----| -│ | 👤 | Carlos Lopez | carlos@empresa.com | 01/12/2025 | 🗑| -│ | 👤 | Maria Garcia | maria@empresa.com | 15/11/2025 | 🗑| -│ | 👤 | Pedro Martinez | pedro@empresa.com | 10/11/2025 | 🗑| -│ │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### API Endpoints - -```typescript -// Desde usuario - asignar roles -PUT /api/v1/users/:userId/roles -{ "roleIds": ["role-1", "role-2"] } - -// Desde rol - asignar usuarios -POST /api/v1/roles/:roleId/users -{ "userIds": ["user-1", "user-2"] } - -// Ver usuarios de un rol -GET /api/v1/roles/:roleId/users - -// Ver permisos efectivos -GET /api/v1/users/:userId/permissions -``` - -### Respuestas - -```typescript -// Asignacion exitosa -{ - "userId": "user-uuid", - "roles": ["admin", "vendedor"], - "effectivePermissions": ["users:*", "sales:*", ...] -} - -// Asignacion masiva -{ - "results": { - "success": ["user-1", "user-2"], - "failed": [ - { "userId": "user-3", "reason": "Usuario no encontrado" } - ] - } -} -``` - ---- - -## Definicion de Done - -- [ ] Endpoint PUT /api/v1/users/:id/roles -- [ ] Endpoint POST /api/v1/roles/:id/users -- [ ] Endpoint GET /api/v1/roles/:id/users -- [ ] Endpoint GET /api/v1/users/:id/permissions -- [ ] Proteccion de rol super_admin -- [ ] Validacion de ultimo super_admin -- [ ] Calculo de permisos efectivos -- [ ] Frontend: RoleAssignmentModal -- [ ] Frontend: UsersOfRole view -- [ ] Tests unitarios -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Endpoints | 4h | -| Backend: Validaciones | 2h | -| Backend: Permisos efectivos | 2h | -| Backend: Tests | 2h | -| Frontend: RoleAssignmentModal | 4h | -| Frontend: BulkAssignment | 2h | -| Frontend: Tests | 2h | -| **Total** | **18h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-004.md b/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-004.md deleted file mode 100644 index 86c1cae..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-003/US-MGN003-004.md +++ /dev/null @@ -1,230 +0,0 @@ -# US-MGN003-004: Control de Acceso RBAC - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN003-004 | -| **Modulo** | MGN-003 Roles/RBAC | -| **Sprint** | Sprint 2 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Estado** | Ready | -| **Autor** | System | -| **Fecha** | 2025-12-05 | - ---- - -## Historia de Usuario - -**Como** desarrollador del sistema -**Quiero** que el sistema valide automaticamente los permisos en cada request -**Para** garantizar que los usuarios solo accedan a funcionalidades autorizadas - ---- - -## Criterios de Aceptacion - -### Escenario 1: Acceso con permiso valido - -```gherkin -Given un usuario con permiso "users:read" - And endpoint GET /api/v1/users requiere "users:read" -When el usuario hace la solicitud -Then el sistema permite el acceso - And retorna status 200 con los datos -``` - -### Escenario 2: Acceso denegado - -```gherkin -Given un usuario con permiso "users:read" solamente - And endpoint DELETE /api/v1/users/:id requiere "users:delete" -When el usuario intenta eliminar -Then el sistema retorna status 403 - And el mensaje es "No tienes permiso para realizar esta accion" - And NO revela que permiso falta (seguridad) -``` - -### Escenario 3: Wildcard permite acceso - -```gherkin -Given un usuario con permiso "users:*" - And endpoint requiere "users:delete" -When el usuario hace la solicitud -Then el sistema permite el acceso - And wildcard cubre el permiso especifico -``` - -### Escenario 4: Super Admin bypass - -```gherkin -Given un usuario con rol "super_admin" - And endpoint requiere "cualquier:permiso" -When el usuario hace la solicitud -Then el sistema permite el acceso - And no valida permisos especificos -``` - -### Escenario 5: Permisos alternativos (OR) - -```gherkin -Given endpoint con @AnyPermission('users:update', 'users:admin') - And usuario tiene solo "users:admin" -When el usuario hace la solicitud -Then el sistema permite el acceso - And basta con tener UNO de los permisos -``` - -### Escenario 6: Owner puede acceder - -```gherkin -Given endpoint PATCH /api/v1/users/:id con @OwnerOrPermission('users:update') - And usuario accede a su propio perfil (id = su id) - And usuario NO tiene permiso "users:update" -When el usuario actualiza su perfil -Then el sistema permite el acceso - And ser owner del recurso es suficiente -``` - -### Escenario 7: Cache de permisos - -```gherkin -Given permisos de usuario cacheados -When el admin cambia roles del usuario -Then el cache se invalida inmediatamente - And siguiente request recalcula permisos -``` - ---- - -## Mockup / Wireframe - -``` -Flujo de validacion (interno del sistema): - -┌─────────────────────────────────────────────────────────────────┐ -│ Request HTTP │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 1. JwtAuthGuard │ │ -│ │ - Valida token JWT │ │ -│ │ - Extrae usuario del token │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 2. TenantGuard │ │ -│ │ - Verifica tenant del usuario │ │ -│ │ - Aplica contexto de tenant │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 3. RbacGuard │ │ -│ │ - Lee decoradores @Permissions, @Roles │ │ -│ │ - Obtiene permisos efectivos (cache 5min) │ │ -│ │ - Valida segun modo (AND/OR) │ │ -│ │ - Super Admin bypass │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ 4. Controller │ │ -│ │ - Ejecuta logica de negocio │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Notas Tecnicas - -### Decoradores Disponibles - -```typescript -// Requiere TODOS los permisos (AND) -@Permissions('users:read', 'users:update') - -// Requiere AL MENOS UNO (OR) -@AnyPermission('users:update', 'users:admin') - -// Requiere uno de los roles -@Roles('admin', 'manager') - -// Ruta publica (sin auth) -@Public() - -// Owner o permiso -@OwnerOrPermission('users:update') -``` - -### Uso en Controllers - -```typescript -@Controller('api/v1/users') -@UseGuards(JwtAuthGuard, TenantGuard, RbacGuard) -export class UsersController { - - @Get() - @Permissions('users:read') - findAll() { } - - @Delete(':id') - @Permissions('users:delete') - remove() { } - - @Patch(':id') - @OwnerOrPermission('users:update') - update() { } -} -``` - -### Cache de Permisos - -```typescript -// TTL: 5 minutos -// Key: rbac:permissions:{userId} -// Invalidacion: al cambiar roles del usuario -``` - ---- - -## Definicion de Done - -- [ ] Decoradores @Permissions, @AnyPermission, @Roles -- [ ] Decorador @Public para rutas sin auth -- [ ] Decorador @OwnerOrPermission -- [ ] RbacGuard implementado -- [ ] OwnerGuard implementado -- [ ] Cache de permisos en Redis -- [ ] Invalidacion de cache automatica -- [ ] Log de accesos denegados -- [ ] Tests unitarios para guards -- [ ] Tests de integracion RBAC -- [ ] Code review aprobado - ---- - -## Estimacion - -| Tarea | Horas | -|-------|-------| -| Backend: Decoradores | 2h | -| Backend: RbacGuard | 4h | -| Backend: OwnerGuard | 2h | -| Backend: Cache service | 2h | -| Backend: Invalidacion cache | 2h | -| Backend: Tests guards | 3h | -| Backend: Tests integracion | 3h | -| **Total** | **18h** | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial | diff --git a/docs/05-user-stories/_legacy_backup/MGN-004/BACKLOG-MGN004.md b/docs/05-user-stories/_legacy_backup/MGN-004/BACKLOG-MGN004.md deleted file mode 100644 index 4273f30..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-004/BACKLOG-MGN004.md +++ /dev/null @@ -1,210 +0,0 @@ -# Backlog MGN-004: Multi-Tenancy - -## Resumen del Modulo - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-004 | -| **Nombre** | Multi-Tenancy | -| **Total User Stories** | 4 | -| **Total Story Points** | 47 | -| **Estado** | Ready for Development | -| **Fecha** | 2025-12-05 | - ---- - -## User Stories - -| ID | Nombre | Prioridad | SP | Estado | -|----|--------|-----------|-----|--------| -| [US-MGN004-001](./US-MGN004-001.md) | Gestion de Tenants | P0 | 13 | Ready | -| [US-MGN004-002](./US-MGN004-002.md) | Configuracion de Tenant | P0 | 8 | Ready | -| [US-MGN004-003](./US-MGN004-003.md) | Aislamiento de Datos | P0 | 13 | Ready | -| [US-MGN004-004](./US-MGN004-004.md) | Subscripciones y Limites | P1 | 13 | Ready | - ---- - -## Arquitectura Multi-Tenant - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Request Flow │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. HTTP Request │ -│ └─> Authorization: Bearer {JWT} │ -│ │ -│ 2. JwtAuthGuard │ -│ └─> Valida token, extrae user + tenant_id │ -│ │ -│ 3. TenantGuard (US-MGN004-003) │ -│ └─> Verifica tenant activo │ -│ └─> Inyecta tenant en request │ -│ │ -│ 4. TenantContextMiddleware (US-MGN004-003) │ -│ └─> SET app.current_tenant_id = :tenantId │ -│ │ -│ 5. LimitGuard (US-MGN004-004) │ -│ └─> Verifica limites de subscripcion │ -│ │ -│ 6. ModuleGuard (US-MGN004-004) │ -│ └─> Verifica acceso al modulo │ -│ │ -│ 7. RbacGuard (MGN-003) │ -│ └─> Valida permisos del usuario │ -│ │ -│ 8. Controller -> Service -> Repository │ -│ │ -│ 9. PostgreSQL RLS (US-MGN004-003) │ -│ └─> WHERE tenant_id = current_tenant_id() │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Estados del Tenant - -``` - ┌──────────────┐ - │ created │ - └──────┬───────┘ - │ (start trial) - ▼ - ┌──────────────┐ - ┌──────────│ trial │──────────┐ - │ └──────┬───────┘ │ - │ │ │ - │ (pay) │ (expire) │ (convert) - │ │ │ - │ ▼ │ - │ ┌──────────────┐ │ - │ │trial_expired │ │ - │ └──────────────┘ │ - │ │ - ▼ ▼ - ┌──────────────┐ ┌──────────────┐ - │ active │◄────────────────────│ active │ - └──────┬───────┘ (reactivate) └──────┬───────┘ - │ │ - │ (suspend) │ - ▼ │ - ┌──────────────┐ │ - │ suspended │────────────────────────────┘ - └──────┬───────┘ - │ (schedule delete) - ▼ - ┌──────────────────┐ - │ pending_deletion │ - └────────┬─────────┘ - │ (30 days) - ▼ - ┌──────────────┐ - │ deleted │ - └──────────────┘ -``` - ---- - -## Planes de Subscripcion - -| Plan | Usuarios | Storage | Precio | -|------|----------|---------|--------| -| Trial | 5 | 1 GB | Gratis (14 dias) | -| Starter | 10 | 5 GB | $29/mes | -| Professional | 50 | 25 GB | $99/mes | -| Enterprise | Ilimitado | 100 GB | $299/mes | - ---- - -## Distribucion de Story Points - -``` -US-MGN004-001 (Gestion Tenants): ████████████████████████████ 13 SP (28%) -US-MGN004-002 (Configuracion): █████████████████ 8 SP (17%) -US-MGN004-003 (Aislamiento RLS): ████████████████████████████ 13 SP (28%) -US-MGN004-004 (Subscripciones): ████████████████████████████ 13 SP (28%) - ───────────────────────────── - Total: 47 SP -``` - ---- - -## Orden de Implementacion Recomendado - -1. **US-MGN004-003** - Aislamiento de Datos (Base para todo) - - RLS policies - - TenantGuard - - TenantContextMiddleware - - TenantBaseEntity - -2. **US-MGN004-001** - Gestion de Tenants (CRUD basico) - - Entidad Tenant - - TenantsService - - Platform Admin endpoints - -3. **US-MGN004-002** - Configuracion de Tenant - - TenantSettings - - Upload de logos - - Merge con defaults - -4. **US-MGN004-004** - Subscripciones y Limites (Monetizacion) - - Plans, Subscriptions - - LimitGuard, ModuleGuard - - Billing integration - ---- - -## Dependencias con Otros Modulos - -| Modulo | Dependencia | -|--------|-------------| -| MGN-001 Auth | JWT debe incluir tenant_id claim | -| MGN-002 Users | Users deben tener tenant_id | -| MGN-003 RBAC | Roles son per-tenant, permisos de platform admin | - ---- - -## Riesgos Identificados - -| Riesgo | Mitigacion | -|--------|------------| -| RLS mal configurado expone datos | Tests exhaustivos de aislamiento | -| Performance con muchos tenants | Indices compuestos, caching | -| Migracion de datos existentes | Script de migracion con tenant default | -| Complejidad de billing | Integracion con Stripe (proven) | - ---- - -## Definition of Done del Modulo - -- [ ] US-MGN004-001: CRUD de tenants completo -- [ ] US-MGN004-002: Settings funcionando con merge -- [ ] US-MGN004-003: RLS aplicado en TODAS las tablas -- [ ] US-MGN004-003: Tests de aislamiento pasando -- [ ] US-MGN004-004: Planes y limites configurados -- [ ] US-MGN004-004: LimitGuard y ModuleGuard activos -- [ ] Tests unitarios > 80% coverage -- [ ] Tests de aislamiento exhaustivos -- [ ] Security review de RLS aprobado -- [ ] Performance tests con multiples tenants -- [ ] Documentacion Swagger completa - ---- - -## Metricas de Exito - -| Metrica | Objetivo | -|---------|----------| -| Tiempo de respuesta (con RLS) | < 100ms p95 | -| Tests de aislamiento | 100% pass | -| Cobertura de tests | > 80% | -| Vulnerabilidades cross-tenant | 0 | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 US | diff --git a/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-001.md b/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-001.md deleted file mode 100644 index e24728b..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-001.md +++ /dev/null @@ -1,184 +0,0 @@ -# US-MGN004-001: Gestion de Tenants - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN004-001 | -| **Modulo** | MGN-004 Tenants | -| **RF Relacionado** | RF-TENANT-001 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 13 | -| **Sprint** | TBD | - ---- - -## Historia de Usuario - -**Como** Platform Admin -**Quiero** gestionar los tenants de la plataforma (crear, ver, editar, suspender, eliminar) -**Para** administrar las organizaciones que usan el sistema ERP - ---- - -## Criterios de Aceptacion - -### AC-001: Listar tenants - -```gherkin -Given soy Platform Admin autenticado -When accedo a GET /api/v1/platform/tenants -Then veo lista paginada de tenants - And cada tenant muestra: nombre, slug, estado, fecha creacion - And puedo filtrar por estado (created, trial, active, suspended) - And puedo buscar por nombre o slug -``` - -### AC-002: Crear nuevo tenant - -```gherkin -Given soy Platform Admin autenticado -When envio POST /api/v1/platform/tenants con: - | name | slug | subdomain | - | Empresa XYZ | empresa-xyz | xyz | -Then se crea el tenant con estado "created" - And se crean los settings por defecto - And se genera registro de auditoria -``` - -### AC-003: Validacion de slug unico - -```gherkin -Given existe tenant con slug "empresa-abc" -When intento crear tenant con slug "empresa-abc" -Then el sistema responde con status 409 - And el mensaje indica que el slug ya existe -``` - -### AC-004: Ver detalle de tenant - -```gherkin -Given soy Platform Admin autenticado - And existe tenant "tenant-123" -When accedo a GET /api/v1/platform/tenants/tenant-123 -Then veo toda la informacion del tenant - And incluye: nombre, slug, subdominio, estado, fechas - And incluye: subscripcion actual, uso de recursos -``` - -### AC-005: Actualizar tenant - -```gherkin -Given soy Platform Admin autenticado - And existe tenant "tenant-123" con nombre "Empresa Original" -When envio PATCH /api/v1/platform/tenants/tenant-123 con: - | name | subdominio | - | Empresa Nueva | nuevo | -Then el nombre cambia a "Empresa Nueva" - And el subdominio cambia a "nuevo" - And se actualiza updated_at y updated_by -``` - -### AC-006: Suspender tenant - -```gherkin -Given soy Platform Admin autenticado - And tenant "tenant-123" esta activo -When cambio estado a "suspended" con razon "Falta de pago" -Then el estado cambia a "suspended" - And se registra suspended_at y suspension_reason - And usuarios del tenant no pueden acceder -``` - -### AC-007: Reactivar tenant suspendido - -```gherkin -Given soy Platform Admin autenticado - And tenant "tenant-123" esta suspendido -When cambio estado a "active" -Then el estado cambia a "active" - And se limpian campos de suspension - And usuarios pueden acceder nuevamente -``` - -### AC-008: Programar eliminacion - -```gherkin -Given soy Platform Admin autenticado - And tenant "tenant-123" existe -When envio DELETE /api/v1/platform/tenants/tenant-123 -Then el estado cambia a "pending_deletion" - And se programa eliminacion en 30 dias - And se notifica al owner del tenant -``` - -### AC-009: Restaurar tenant pendiente de eliminacion - -```gherkin -Given soy Platform Admin autenticado - And tenant "tenant-123" esta en "pending_deletion" -When envio POST /api/v1/platform/tenants/tenant-123/restore -Then el estado cambia a "active" - And se cancela la eliminacion programada -``` - -### AC-010: Cambiar contexto a tenant (switch) - -```gherkin -Given soy Platform Admin autenticado -When envio POST /api/v1/platform/switch-tenant/tenant-123 -Then mi contexto cambia a tenant "tenant-123" - And puedo ver datos de ese tenant - And la accion queda auditada -``` - ---- - -## Tareas Tecnicas - -| ID | Tarea | Estimacion | -|----|-------|------------| -| T-001 | Crear entidad Tenant con TypeORM | 1 SP | -| T-002 | Implementar TenantsService CRUD | 3 SP | -| T-003 | Implementar PlatformTenantsController | 2 SP | -| T-004 | Crear DTOs con validaciones | 1 SP | -| T-005 | Implementar transicion de estados | 2 SP | -| T-006 | Implementar switch tenant | 2 SP | -| T-007 | Tests unitarios | 2 SP | -| **Total** | | **13 SP** | - ---- - -## Notas de Implementacion - -- Estados validos: created -> trial -> active -> suspended -> pending_deletion -> deleted -- Solo Platform Admin puede gestionar tenants -- Switch tenant debe quedar en logs de auditoria -- Eliminacion real solo despues de 30 dias de grace period - ---- - -## Mockup de Referencia - -Ver RF-TENANT-001.md seccion Mockup. - ---- - -## Dependencias - -| Tipo | Descripcion | -|------|-------------| -| Backend | Schema core_tenants creado | -| Auth | JWT con claims de platform_admin | -| Audit | Sistema de auditoria funcionando | - ---- - -## Definition of Done - -- [ ] Endpoints implementados y documentados en Swagger -- [ ] Validaciones de DTOs completas -- [ ] Transiciones de estado implementadas -- [ ] Tests unitarios con >80% coverage -- [ ] Code review aprobado -- [ ] Logs de auditoria funcionando diff --git a/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-002.md b/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-002.md deleted file mode 100644 index 954a670..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-002.md +++ /dev/null @@ -1,178 +0,0 @@ -# US-MGN004-002: Configuracion de Tenant - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN004-002 | -| **Modulo** | MGN-004 Tenants | -| **RF Relacionado** | RF-TENANT-002 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 8 | -| **Sprint** | TBD | - ---- - -## Historia de Usuario - -**Como** Tenant Admin -**Quiero** configurar la informacion de mi organizacion (empresa, branding, regional, seguridad) -**Para** personalizar el sistema segun las necesidades de mi empresa - ---- - -## Criterios de Aceptacion - -### AC-001: Ver configuracion actual - -```gherkin -Given soy Tenant Admin autenticado -When accedo a GET /api/v1/tenant/settings -Then veo configuracion completa organizada en secciones: - | company | branding | regional | operational | security | - And valores propios sobrescriben defaults - And se indica cuales son valores heredados -``` - -### AC-002: Actualizar informacion de empresa - -```gherkin -Given soy Tenant Admin autenticado -When envio PATCH /api/v1/tenant/settings con: - | company.companyName | company.taxId | - | Mi Empresa S.A. | MEMP850101ABC | -Then se actualizan los campos enviados - And otros campos de company no se modifican - And se actualiza updated_at y updated_by -``` - -### AC-003: Personalizar branding - -```gherkin -Given soy Tenant Admin autenticado -When actualizo branding con: - | primaryColor | secondaryColor | - | #FF5733 | #33FF57 | -Then los colores se guardan - And la UI refleja los nuevos colores -``` - -### AC-004: Subir logo - -```gherkin -Given soy Tenant Admin autenticado -When envio POST /api/v1/tenant/settings/logo con imagen PNG de 500KB -Then el logo se almacena en storage - And se genera version thumbnail - And se actualiza branding.logo con URL - And el logo aparece en la UI -``` - -### AC-005: Validar formato y tamano de logo - -```gherkin -Given soy Tenant Admin autenticado -When intento subir logo de 10MB -Then el sistema responde con status 400 - And el mensaje indica "Tamano maximo: 5MB" -``` - -### AC-006: Configurar zona horaria - -```gherkin -Given soy Tenant Admin autenticado -When actualizo regional.defaultTimezone a "America/New_York" -Then las fechas se muestran en hora de Nueva York - And los reportes usan esa zona horaria -``` - -### AC-007: Configurar politicas de seguridad - -```gherkin -Given soy Tenant Admin autenticado -When actualizo security con: - | passwordMinLength | mfaRequired | - | 12 | true | -Then nuevos usuarios deben usar password de 12+ caracteres - And se requiere MFA para todos los usuarios - And usuarios existentes deben activar MFA en siguiente login -``` - -### AC-008: Resetear a valores por defecto - -```gherkin -Given soy Tenant Admin con branding personalizado -When envio POST /api/v1/tenant/settings/reset con: - | sections | ["branding"] | -Then branding vuelve a valores por defecto de plataforma - And otras secciones no se modifican -``` - -### AC-009: Validacion de campos - -```gherkin -Given soy Tenant Admin autenticado -When envio color con formato invalido "#GGG" -Then el sistema responde con status 400 - And el mensaje indica formato invalido -``` - -### AC-010: Herencia de defaults - -```gherkin -Given tenant sin configuracion de dateFormat - And plataforma tiene default "DD/MM/YYYY" -When consulto settings -Then dateFormat muestra "DD/MM/YYYY" - And se indica que es valor heredado (_inherited) -``` - ---- - -## Tareas Tecnicas - -| ID | Tarea | Estimacion | -|----|-------|------------| -| T-001 | Crear entidad TenantSettings | 1 SP | -| T-002 | Implementar TenantSettingsService | 2 SP | -| T-003 | Implementar merge con defaults | 1 SP | -| T-004 | Crear endpoint de upload de logo | 1 SP | -| T-005 | Implementar reset a defaults | 1 SP | -| T-006 | Tests unitarios | 2 SP | -| **Total** | | **8 SP** | - ---- - -## Notas de Implementacion - -- Settings se almacenan como JSONB en PostgreSQL -- Merge inteligente: solo sobrescribir campos enviados -- Logo se almacena en storage externo (S3, GCS, etc.) -- Cambios de seguridad aplican en siguiente login - ---- - -## Mockup de Referencia - -Ver RF-TENANT-002.md seccion Mockup. - ---- - -## Dependencias - -| Tipo | Descripcion | -|------|-------------| -| Backend | Tenant existente (US-MGN004-001) | -| Storage | Servicio de storage configurado | -| Config | Defaults de plataforma definidos | - ---- - -## Definition of Done - -- [ ] CRUD de settings funcionando -- [ ] Merge con defaults implementado -- [ ] Upload de logo con validaciones -- [ ] Reset a defaults funcionando -- [ ] Validaciones de DTOs completas -- [ ] Tests unitarios con >80% coverage diff --git a/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-003.md b/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-003.md deleted file mode 100644 index aad30c0..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-003.md +++ /dev/null @@ -1,205 +0,0 @@ -# US-MGN004-003: Aislamiento de Datos Multi-Tenant - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN004-003 | -| **Modulo** | MGN-004 Tenants | -| **RF Relacionado** | RF-TENANT-003 | -| **Prioridad** | P0 - Critica | -| **Story Points** | 13 | -| **Sprint** | TBD | - ---- - -## Historia de Usuario - -**Como** Usuario del sistema -**Quiero** que mis datos esten completamente aislados de otros tenants -**Para** garantizar la seguridad y privacidad de la informacion de mi organizacion - ---- - -## Criterios de Aceptacion - -### AC-001: Usuario solo ve datos de su tenant - -```gherkin -Given usuario de Tenant A autenticado - And 100 productos en Tenant A - And 50 productos en Tenant B -When consulta GET /api/v1/products -Then solo ve los 100 productos de Tenant A - And no ve ningun producto de Tenant B -``` - -### AC-002: No acceso a recurso de otro tenant - -```gherkin -Given usuario de Tenant A autenticado - And producto "P-001" pertenece a Tenant B -When intenta GET /api/v1/products/P-001 -Then el sistema responde con status 404 - And el mensaje es "Recurso no encontrado" - And NO revela que el recurso existe en otro tenant -``` - -### AC-003: Asignacion automatica de tenant_id - -```gherkin -Given usuario de Tenant A autenticado -When crea un producto sin especificar tenant_id -Then el sistema asigna automaticamente tenant_id de Tenant A - And el producto queda en Tenant A -``` - -### AC-004: No permitir modificar tenant_id - -```gherkin -Given usuario de Tenant A autenticado - And producto existente en Tenant A -When intenta actualizar tenant_id a Tenant B -Then el sistema responde con status 400 - And el mensaje es "No se puede cambiar el tenant de un recurso" -``` - -### AC-005: RLS previene acceso sin contexto - -```gherkin -Given conexion a base de datos sin app.current_tenant_id -When se ejecuta SELECT * FROM core_users.users -Then el resultado es vacio - And no se produce error - And RLS previene acceso a datos -``` - -### AC-006: TenantGuard valida tenant activo - -```gherkin -Given usuario con token JWT valido - And tenant en estado "suspended" -When intenta acceder a cualquier endpoint -Then el sistema responde con status 403 - And el mensaje indica "Tenant suspendido" -``` - -### AC-007: TenantGuard detecta trial expirado - -```gherkin -Given usuario de tenant en estado "trial" - And trial_ends_at fue hace 2 dias -When intenta acceder a cualquier endpoint -Then el sistema responde con status 403 - And el mensaje indica "Periodo de prueba expirado" -``` - -### AC-008: Platform Admin switch explicito - -```gherkin -Given Platform Admin autenticado - And tiene acceso a todos los tenants -When NO ha hecho switch a ningun tenant -Then no puede ver datos de ningun tenant - And debe hacer POST /api/v1/platform/switch-tenant/:id primero -``` - -### AC-009: Middleware setea contexto PostgreSQL - -```gherkin -Given request autenticado con tenant_id en JWT -When pasa por TenantContextMiddleware -Then se ejecuta SET app.current_tenant_id = :tenantId - And todas las queries posteriores filtran por ese tenant -``` - -### AC-010: Indices optimizados por tenant - -```gherkin -Given tabla users con 1M registros distribuidos en 100 tenants -When usuario de Tenant A busca por email -Then la query usa indice compuesto (tenant_id, email) - And tiempo de respuesta < 50ms -``` - ---- - -## Tareas Tecnicas - -| ID | Tarea | Estimacion | -|----|-------|------------| -| T-001 | Crear migracion RLS policies | 3 SP | -| T-002 | Implementar TenantGuard | 2 SP | -| T-003 | Implementar TenantContextMiddleware | 2 SP | -| T-004 | Crear TenantBaseEntity | 1 SP | -| T-005 | Crear TenantAwareService base | 2 SP | -| T-006 | Tests de aislamiento | 3 SP | -| **Total** | | **13 SP** | - ---- - -## Notas de Implementacion - -### Migracion RLS - -```sql --- Habilitar RLS en tablas -ALTER TABLE core_users.users ENABLE ROW LEVEL SECURITY; - --- Funcion para obtener tenant actual -CREATE OR REPLACE FUNCTION current_tenant_id() -RETURNS UUID AS $$ -BEGIN - RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID; -END; -$$ LANGUAGE plpgsql STABLE; - --- Politica de SELECT -CREATE POLICY tenant_isolation_select ON core_users.users - FOR SELECT USING (tenant_id = current_tenant_id()); - --- Politica de INSERT -CREATE POLICY tenant_isolation_insert ON core_users.users - FOR INSERT WITH CHECK (tenant_id = current_tenant_id()); -``` - -### TenantBaseEntity - -```typescript -export abstract class TenantBaseEntity { - @Column({ name: 'tenant_id' }) - tenantId: string; - - @ManyToOne(() => Tenant) - @JoinColumn({ name: 'tenant_id' }) - tenant: Tenant; -} -``` - ---- - -## Mockup de Referencia - -Ver RF-TENANT-003.md diagramas de arquitectura. - ---- - -## Dependencias - -| Tipo | Descripcion | -|------|-------------| -| Database | PostgreSQL 12+ con RLS | -| Auth | JWT con tenant_id claim | -| Backend | Schema core_tenants creado | - ---- - -## Definition of Done - -- [ ] RLS habilitado en TODAS las tablas de negocio -- [ ] TenantGuard implementado y aplicado globalmente -- [ ] TenantContextMiddleware seteando variable de sesion -- [ ] Tests de aislamiento pasando (cross-tenant access blocked) -- [ ] Performance test con queries filtradas por tenant -- [ ] Security review aprobado -- [ ] Documentacion de patrones para nuevas tablas diff --git a/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-004.md b/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-004.md deleted file mode 100644 index b5a3933..0000000 --- a/docs/05-user-stories/_legacy_backup/MGN-004/US-MGN004-004.md +++ /dev/null @@ -1,211 +0,0 @@ -# US-MGN004-004: Subscripciones y Limites - -## Identificacion - -| Campo | Valor | -|-------|-------| -| **ID** | US-MGN004-004 | -| **Modulo** | MGN-004 Tenants | -| **RF Relacionado** | RF-TENANT-004 | -| **Prioridad** | P1 - Alta | -| **Story Points** | 13 | -| **Sprint** | TBD | - ---- - -## Historia de Usuario - -**Como** Tenant Admin -**Quiero** gestionar la subscripcion de mi organizacion y ver los limites de uso -**Para** controlar los costos y asegurar que tengo los recursos necesarios - ---- - -## Criterios de Aceptacion - -### AC-001: Ver subscripcion actual - -```gherkin -Given soy Tenant Admin autenticado - And tengo plan Professional activo -When accedo a GET /api/v1/tenant/subscription -Then veo: - | plan | Professional | - | price | $99 USD/mes | - | status | active | - | nextRenewal | 01/01/2026 | - And veo uso actual vs limites -``` - -### AC-002: Ver uso de recursos - -```gherkin -Given soy Tenant Admin con plan Professional (50 usuarios, 25GB) - And tengo 35 usuarios activos y 18GB usado -When consulto GET /api/v1/tenant/subscription/usage -Then veo: - | recurso | actual | limite | porcentaje | - | usuarios | 35 | 50 | 70% | - | storage | 18GB | 25GB | 72% | - | apiCalls | 5000 | 50000 | 10% | -``` - -### AC-003: Bloqueo por limite de usuarios - -```gherkin -Given tenant con plan Starter (10 usuarios) - And 10 usuarios activos (100%) -When Admin intenta crear usuario #11 -Then el sistema responde con status 402 - And el mensaje indica limite alcanzado - And sugiere planes con mayor limite -``` - -### AC-004: Bloqueo por modulo no incluido - -```gherkin -Given tenant con plan Starter (sin CRM) -When usuario intenta GET /api/v1/crm/contacts -Then el sistema responde con status 402 - And el mensaje indica modulo no disponible - And sugiere planes que incluyen CRM -``` - -### AC-005: Ver planes disponibles - -```gherkin -Given soy usuario autenticado -When accedo a GET /api/v1/subscription/plans -Then veo lista de planes publicos - And cada plan muestra: nombre, precio, limites, features - And puedo comparar con mi plan actual -``` - -### AC-006: Upgrade de plan - -```gherkin -Given tenant en Starter ($29/mes) al dia 15 del mes -When solicito upgrade a Professional ($99/mes) -Then sistema calcula prorrateo: $35 (15 dias de diferencia) - And proceso pago de $35 - And plan cambia inmediatamente a Professional - And nuevos limites aplican de inmediato -``` - -### AC-007: Cancelar subscripcion - -```gherkin -Given soy Tenant Admin con subscripcion activa -When solicito cancelar subscripcion -Then sistema muestra encuesta de salida - And confirmo cancelacion - And subscripcion se marca cancel_at_period_end = true - And acceso continua hasta fin de periodo pagado -``` - -### AC-008: Trial expira - -```gherkin -Given tenant en Trial de 14 dias -When pasan los 14 dias sin subscribirse -Then estado cambia a "trial_expired" - And usuarios no pueden acceder (excepto admin para subscribirse) - And datos se conservan -``` - -### AC-009: Verificar limite antes de accion - -```gherkin -Given endpoint que crea usuarios - And tiene decorador @CheckLimit('users') -When se ejecuta la accion -Then LimitGuard verifica limite automaticamente - And si excede, retorna 402 antes de ejecutar logica -``` - -### AC-010: Ver historial de facturas - -```gherkin -Given soy Tenant Admin autenticado -When accedo a GET /api/v1/tenant/invoices -Then veo lista de facturas: - | fecha | concepto | monto | estado | - | 01/12/2025 | Professional - Dic | $99.00 | Pagado | - And puedo descargar PDF de cada factura -``` - ---- - -## Tareas Tecnicas - -| ID | Tarea | Estimacion | -|----|-------|------------| -| T-001 | Crear entidades Plan, Subscription, Invoice | 2 SP | -| T-002 | Implementar SubscriptionsService | 3 SP | -| T-003 | Implementar LimitGuard | 2 SP | -| T-004 | Implementar ModuleGuard | 1 SP | -| T-005 | Implementar TenantUsageService | 2 SP | -| T-006 | Crear endpoints de subscripcion | 2 SP | -| T-007 | Tests unitarios | 2 SP | -| **Total** | | **14 SP** | - ---- - -## Notas de Implementacion - -### Decorador CheckLimit - -```typescript -@Post() -@CheckLimit('users') -create(@Body() dto: CreateUserDto) { } -``` - -### Decorador CheckModule - -```typescript -@Get() -@CheckModule('crm') -findAllContacts() { } -``` - -### Response 402 Payment Required - -```json -{ - "statusCode": 402, - "error": "Payment Required", - "message": "Limite de usuarios alcanzado (10/10)", - "upgradeOptions": [ - { "planId": "...", "name": "Professional", "newLimit": 50 } - ] -} -``` - ---- - -## Mockup de Referencia - -Ver RF-TENANT-004.md seccion Mockup. - ---- - -## Dependencias - -| Tipo | Descripcion | -|------|-------------| -| Backend | Tenants y Settings (US-MGN004-001, US-MGN004-002) | -| Payment | Integracion con Stripe/PayPal (futuro) | -| Scheduler | Jobs para renovacion y notificaciones | - ---- - -## Definition of Done - -- [ ] Planes y subscripciones funcionando -- [ ] LimitGuard bloqueando excesos -- [ ] ModuleGuard verificando acceso a modulos -- [ ] Upgrade/downgrade funcionando -- [ ] Calculo de prorrateo correcto -- [ ] Historial de facturas visible -- [ ] Tests unitarios con >80% coverage diff --git a/docs/05-user-stories/mgn-001/BACKLOG-MGN-001.md b/docs/05-user-stories/mgn-001/BACKLOG-MGN-001.md deleted file mode 100644 index b94be00..0000000 --- a/docs/05-user-stories/mgn-001/BACKLOG-MGN-001.md +++ /dev/null @@ -1,162 +0,0 @@ -# Backlog del Modulo MGN-001: Auth - -## Resumen - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-001 | -| **Nombre** | Auth - Autenticacion | -| **Total User Stories** | 4 | -| **Total Story Points** | 29 | -| **Estado** | En documentacion | -| **Fecha** | 2025-12-05 | - ---- - -## User Stories - -### Sprint 1 - Foundation (21 SP) - -| ID | Nombre | SP | Prioridad | Estado | Asignado | -|----|--------|-----|-----------|--------|----------| -| [US-MGN001-001](./US-MGN001-001.md) | Login con Email y Password | 8 | P0 | Ready | - | -| [US-MGN001-002](./US-MGN001-002.md) | Logout y Cierre de Sesion | 5 | P0 | Ready | - | -| [US-MGN001-003](./US-MGN001-003.md) | Renovacion Automatica de Tokens | 8 | P0 | Ready | - | - -### Sprint 2 - Enhancement (8 SP) - -| ID | Nombre | SP | Prioridad | Estado | Asignado | -|----|--------|-----|-----------|--------|----------| -| [US-MGN001-004](./US-MGN001-004.md) | Recuperacion de Password | 8 | P1 | Ready | - | - ---- - -## Roadmap Visual - -``` -Sprint 1 Sprint 2 -├─────────────────────────────────┼─────────────────────────────────┤ -│ US-001: Login [8 SP] │ US-004: Password Recovery [8 SP]│ -│ US-002: Logout [5 SP] │ │ -│ US-003: Token Refresh [8 SP] │ │ -├─────────────────────────────────┼─────────────────────────────────┤ -│ Total: 21 SP │ Total: 8 SP │ -└─────────────────────────────────┴─────────────────────────────────┘ -``` - ---- - -## Dependencias entre Stories - -``` -US-MGN001-001 (Login) - │ - ├──────────────────┐ - │ │ - ▼ ▼ -US-MGN001-002 (Logout) US-MGN001-003 (Refresh) - │ - │ - ▼ -US-MGN001-004 (Password Recovery) - │ - └── Usa logout-all -``` - ---- - -## Criterios de Aceptacion del Modulo - -### Funcionalidad - -- [ ] Los usuarios pueden autenticarse con email y password -- [ ] Los usuarios pueden cerrar sesion de forma segura -- [ ] Las sesiones se renuevan automaticamente -- [ ] Los usuarios pueden recuperar su password - -### Seguridad - -- [ ] Passwords hasheados con bcrypt (salt rounds = 12) -- [ ] Tokens JWT firmados con RS256 -- [ ] Refresh token en cookie httpOnly -- [ ] Deteccion de token replay -- [ ] Rate limiting implementado -- [ ] Bloqueo de cuenta por intentos fallidos -- [ ] No se revela existencia de emails - -### Performance - -- [ ] Login < 500ms -- [ ] Refresh < 200ms -- [ ] Blacklist en Redis - -### Auditoria - -- [ ] Todos los logins registrados -- [ ] Todos los logouts registrados -- [ ] Intentos fallidos registrados -- [ ] Cambios de password registrados - ---- - -## Metricas del Modulo - -### Cobertura de Codigo - -| Capa | Objetivo | Actual | -|------|----------|--------| -| Backend Services | 80% | - | -| Backend Controllers | 70% | - | -| Frontend Services | 70% | - | -| Frontend Components | 60% | - | - -### Performance - -| Endpoint | Objetivo | P95 | -|----------|----------|-----| -| POST /auth/login | < 500ms | - | -| POST /auth/refresh | < 200ms | - | -| POST /auth/logout | < 200ms | - | -| POST /password/request-reset | < 1000ms | - | - ---- - -## Riesgos - -| Riesgo | Probabilidad | Impacto | Mitigacion | -|--------|--------------|---------|------------| -| Vulnerabilidad en JWT | Media | Alto | Code review, testing seguridad | -| Token replay attack | Baja | Alto | Deteccion de familia, rotacion | -| Email delivery failure | Media | Medio | Retry queue, monitoring | -| Brute force en login | Media | Medio | Rate limiting, CAPTCHA | - ---- - -## Definition of Done del Modulo - -- [ ] Todas las User Stories completadas -- [ ] Tests unitarios > 80% coverage -- [ ] Tests e2e pasando -- [ ] Documentacion Swagger completa -- [ ] Code review aprobado -- [ ] Security review aprobado -- [ ] Performance review aprobado -- [ ] Despliegue en staging exitoso -- [ ] UAT aprobado - ---- - -## Notas del Product Owner - -- El login es bloqueante para todo el proyecto -- Password recovery puede esperar a Sprint 2 -- Priorizar seguridad sobre features adicionales -- Considerar 2FA para fase posterior (no en MVP) - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 US | diff --git a/docs/05-user-stories/mgn-002/BACKLOG-MGN-002.md b/docs/05-user-stories/mgn-002/BACKLOG-MGN-002.md deleted file mode 100644 index a4e5267..0000000 --- a/docs/05-user-stories/mgn-002/BACKLOG-MGN-002.md +++ /dev/null @@ -1,138 +0,0 @@ -# Backlog del Modulo MGN-002: Users - -## Resumen - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-002 | -| **Nombre** | Users - Gestion de Usuarios | -| **Total User Stories** | 4 | -| **Total Story Points** | 21 | -| **Estado** | En documentacion | -| **Fecha** | 2025-12-05 | - ---- - -## User Stories - -### Sprint 2 - Core Users (16 SP) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| [US-MGN002-001](./US-MGN002-001.md) | CRUD de Usuarios (Admin) | 8 | P0 | Ready | -| [US-MGN002-002](./US-MGN002-002.md) | Perfil de Usuario | 5 | P1 | Ready | -| [US-MGN002-003](./US-MGN002-003.md) | Cambio de Password | 3 | P0 | Ready | - -### Sprint 3 - Personalization (5 SP) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| [US-MGN002-004](./US-MGN002-004.md) | Preferencias de Usuario | 5 | P2 | Ready | - ---- - -## Stories Adicionales (No incluidas en scope inicial) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| US-MGN002-005 | Cambio de Email | 5 | P1 | Backlog | -| US-MGN002-006 | Activacion de Cuenta | 3 | P0 | Backlog | -| US-MGN002-007 | Export de Usuarios (CSV) | 3 | P2 | Backlog | -| US-MGN002-008 | Import de Usuarios (CSV) | 5 | P2 | Backlog | - ---- - -## Roadmap Visual - -``` -Sprint 2 Sprint 3 -├─────────────────────────────────┼─────────────────────────────────┤ -│ US-001: CRUD Usuarios [8 SP] │ US-004: Preferencias [5 SP] │ -│ US-002: Perfil [5 SP] │ │ -│ US-003: Cambio Password [3 SP] │ │ -├─────────────────────────────────┼─────────────────────────────────┤ -│ Total: 16 SP │ Total: 5 SP │ -└─────────────────────────────────┴─────────────────────────────────┘ -``` - ---- - -## Dependencias entre Stories - -``` -RF-AUTH-001 (Login) ─────────────────────────────────────────┐ - │ │ - ▼ │ -US-MGN002-001 (CRUD Admin) ──────────────────────────────────┤ - │ │ - ├──────────────────────────────────────────┐ │ - │ │ │ - ▼ ▼ │ -US-MGN002-002 (Perfil) US-MGN002-003 (Pass) │ - │ │ - └───────────────────────┬──────────────────────────────┘ - │ - ▼ - US-MGN002-004 (Preferencias) -``` - ---- - -## Criterios de Aceptacion del Modulo - -### Funcionalidad - -- [ ] Admins pueden crear, listar, editar y eliminar usuarios -- [ ] Usuarios pueden ver y editar su propio perfil -- [ ] Usuarios pueden cambiar su password -- [ ] Usuarios pueden subir avatar -- [ ] Usuarios pueden configurar preferencias - -### Seguridad - -- [ ] Soft delete en lugar de hard delete -- [ ] Solo admins gestionan otros usuarios -- [ ] Password actual requerido para cambios sensibles -- [ ] Historial de passwords para evitar reuso -- [ ] RBAC en todos los endpoints admin - -### UX - -- [ ] Paginacion y filtros en listados -- [ ] Busqueda por nombre y email -- [ ] Avatar con resize automatico -- [ ] Preferencias aplicadas inmediatamente - ---- - -## Estimacion Total - -| Capa | Story Points | -|------|--------------| -| Database | 6 | -| Backend | 15 | -| Frontend | 16 | -| **Total** | **37** | - -> Nota: Esta estimacion corresponde a los 5 RFs completos, no solo las 4 US principales. - ---- - -## Definition of Done del Modulo - -- [ ] Todas las User Stories completadas -- [ ] Tests unitarios > 80% coverage -- [ ] Tests e2e pasando -- [ ] Documentacion Swagger completa -- [ ] Code review aprobado -- [ ] Security review aprobado -- [ ] Despliegue en staging exitoso -- [ ] UAT aprobado - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 US | diff --git a/docs/05-user-stories/mgn-003/BACKLOG-MGN-003.md b/docs/05-user-stories/mgn-003/BACKLOG-MGN-003.md deleted file mode 100644 index 09b5bb1..0000000 --- a/docs/05-user-stories/mgn-003/BACKLOG-MGN-003.md +++ /dev/null @@ -1,177 +0,0 @@ -# Backlog del Modulo MGN-003: Roles/RBAC - -## Resumen - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-003 | -| **Nombre** | Roles/RBAC - Control de Acceso | -| **Total User Stories** | 4 | -| **Total Story Points** | 29 | -| **Estado** | En documentacion | -| **Fecha** | 2025-12-05 | - ---- - -## User Stories - -### Sprint 2 - Core RBAC (29 SP) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| [US-MGN003-001](./US-MGN003-001.md) | CRUD de Roles | 8 | P0 | Ready | -| [US-MGN003-002](./US-MGN003-002.md) | Gestion de Permisos | 5 | P0 | Ready | -| [US-MGN003-003](./US-MGN003-003.md) | Asignacion Roles-Usuarios | 8 | P0 | Ready | -| [US-MGN003-004](./US-MGN003-004.md) | Control de Acceso RBAC | 8 | P0 | Ready | - ---- - -## Stories Adicionales (No incluidas en scope inicial) - -| ID | Nombre | SP | Prioridad | Estado | -|----|--------|-----|-----------|--------| -| US-MGN003-005 | Roles Temporales | 3 | P2 | Backlog | -| US-MGN003-006 | Herencia de Roles | 5 | P2 | Backlog | -| US-MGN003-007 | Auditoria de Cambios RBAC | 3 | P1 | Backlog | -| US-MGN003-008 | Permisos Contextuales | 5 | P2 | Backlog | - ---- - -## Roadmap Visual - -``` -Sprint 2 -├─────────────────────────────────────────────────────────────────┤ -│ US-001: CRUD de Roles [8 SP] │ -│ US-002: Gestion de Permisos [5 SP] │ -│ US-003: Asignacion Roles-Usuarios [8 SP] │ -│ US-004: Control de Acceso RBAC [8 SP] │ -├─────────────────────────────────────────────────────────────────┤ -│ Total: 29 SP │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Dependencias entre Stories - -``` - ┌─────────────────┐ - │ MGN-001 Auth │ - │ (JWT/Login) │ - └────────┬────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ US-MGN003-002 │ - │ Gestion de Permisos │ - │ (Catalogo de permisos) │ - └────────────┬─────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ US-MGN003-001 │ - │ CRUD de Roles │ - │ (Roles con permisos) │ - └────────────┬─────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ US-MGN003-003 │ - │ Asignacion Roles │ - │ (Usuarios con roles) │ - └────────────┬─────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ US-MGN003-004 │ - │ Control de Acceso │ - │ (Guards y decoradores) │ - └────────────┬─────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Todos los endpoints │ - │ del sistema usan RBAC │ - └──────────────────────────┘ -``` - ---- - -## Criterios de Aceptacion del Modulo - -### Funcionalidad - -- [ ] Admins pueden crear, listar, editar y eliminar roles personalizados -- [ ] Roles built-in no pueden eliminarse ni modificarse (solo extender) -- [ ] Permisos se muestran agrupados por modulo -- [ ] Usuarios pueden tener multiples roles -- [ ] Permisos efectivos son la union de todos los roles -- [ ] Wildcards funcionan correctamente (users:* -> users:read, etc.) -- [ ] Control de acceso en TODOS los endpoints protegidos - -### Seguridad - -- [ ] Solo super_admin puede asignar rol super_admin -- [ ] Al menos un super_admin debe existir siempre -- [ ] Errores 403 no revelan que permiso falta -- [ ] Logs de acceso denegado para auditoria -- [ ] Cache de permisos se invalida al cambiar roles - -### Performance - -- [ ] Permisos cacheados por 5 minutos -- [ ] Validacion de permisos < 10ms (desde cache) -- [ ] Invalidacion de cache inmediata - ---- - -## Estimacion Total - -| Capa | Story Points | -|------|--------------| -| Backend: Endpoints | 10 | -| Backend: Guards/Decorators | 8 | -| Backend: Cache | 4 | -| Backend: Tests | 7 | -| Frontend: Pages | 8 | -| Frontend: Components | 6 | -| Frontend: Tests | 5 | -| **Total** | **48** | - -> Nota: Las 4 US principales suman 29 SP. La estimacion detallada de 48 SP incluye integracion y overhead. - ---- - -## Definition of Done del Modulo - -- [ ] Todas las User Stories completadas -- [ ] Permisos seeded en base de datos -- [ ] Roles built-in creados para cada tenant -- [ ] Guards aplicados a todos los endpoints existentes -- [ ] Tests unitarios > 80% coverage -- [ ] Tests e2e de flujos RBAC -- [ ] Documentacion Swagger completa -- [ ] Code review aprobado -- [ ] Security review aprobado -- [ ] Despliegue en staging exitoso -- [ ] UAT aprobado - ---- - -## Riesgos y Mitigaciones - -| Riesgo | Impacto | Probabilidad | Mitigacion | -|--------|---------|--------------|------------| -| Performance de validacion | Alto | Media | Cache de permisos | -| Cache desincronizado | Alto | Baja | Invalidacion inmediata | -| Configuracion incorrecta | Medio | Media | Tests exhaustivos | -| Bloqueo de usuarios | Alto | Baja | Super Admin bypass | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 US | diff --git a/docs/05-user-stories/mgn-004/BACKLOG-MGN-004.md b/docs/05-user-stories/mgn-004/BACKLOG-MGN-004.md deleted file mode 100644 index 4273f30..0000000 --- a/docs/05-user-stories/mgn-004/BACKLOG-MGN-004.md +++ /dev/null @@ -1,210 +0,0 @@ -# Backlog MGN-004: Multi-Tenancy - -## Resumen del Modulo - -| Campo | Valor | -|-------|-------| -| **Modulo** | MGN-004 | -| **Nombre** | Multi-Tenancy | -| **Total User Stories** | 4 | -| **Total Story Points** | 47 | -| **Estado** | Ready for Development | -| **Fecha** | 2025-12-05 | - ---- - -## User Stories - -| ID | Nombre | Prioridad | SP | Estado | -|----|--------|-----------|-----|--------| -| [US-MGN004-001](./US-MGN004-001.md) | Gestion de Tenants | P0 | 13 | Ready | -| [US-MGN004-002](./US-MGN004-002.md) | Configuracion de Tenant | P0 | 8 | Ready | -| [US-MGN004-003](./US-MGN004-003.md) | Aislamiento de Datos | P0 | 13 | Ready | -| [US-MGN004-004](./US-MGN004-004.md) | Subscripciones y Limites | P1 | 13 | Ready | - ---- - -## Arquitectura Multi-Tenant - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Request Flow │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. HTTP Request │ -│ └─> Authorization: Bearer {JWT} │ -│ │ -│ 2. JwtAuthGuard │ -│ └─> Valida token, extrae user + tenant_id │ -│ │ -│ 3. TenantGuard (US-MGN004-003) │ -│ └─> Verifica tenant activo │ -│ └─> Inyecta tenant en request │ -│ │ -│ 4. TenantContextMiddleware (US-MGN004-003) │ -│ └─> SET app.current_tenant_id = :tenantId │ -│ │ -│ 5. LimitGuard (US-MGN004-004) │ -│ └─> Verifica limites de subscripcion │ -│ │ -│ 6. ModuleGuard (US-MGN004-004) │ -│ └─> Verifica acceso al modulo │ -│ │ -│ 7. RbacGuard (MGN-003) │ -│ └─> Valida permisos del usuario │ -│ │ -│ 8. Controller -> Service -> Repository │ -│ │ -│ 9. PostgreSQL RLS (US-MGN004-003) │ -│ └─> WHERE tenant_id = current_tenant_id() │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Estados del Tenant - -``` - ┌──────────────┐ - │ created │ - └──────┬───────┘ - │ (start trial) - ▼ - ┌──────────────┐ - ┌──────────│ trial │──────────┐ - │ └──────┬───────┘ │ - │ │ │ - │ (pay) │ (expire) │ (convert) - │ │ │ - │ ▼ │ - │ ┌──────────────┐ │ - │ │trial_expired │ │ - │ └──────────────┘ │ - │ │ - ▼ ▼ - ┌──────────────┐ ┌──────────────┐ - │ active │◄────────────────────│ active │ - └──────┬───────┘ (reactivate) └──────┬───────┘ - │ │ - │ (suspend) │ - ▼ │ - ┌──────────────┐ │ - │ suspended │────────────────────────────┘ - └──────┬───────┘ - │ (schedule delete) - ▼ - ┌──────────────────┐ - │ pending_deletion │ - └────────┬─────────┘ - │ (30 days) - ▼ - ┌──────────────┐ - │ deleted │ - └──────────────┘ -``` - ---- - -## Planes de Subscripcion - -| Plan | Usuarios | Storage | Precio | -|------|----------|---------|--------| -| Trial | 5 | 1 GB | Gratis (14 dias) | -| Starter | 10 | 5 GB | $29/mes | -| Professional | 50 | 25 GB | $99/mes | -| Enterprise | Ilimitado | 100 GB | $299/mes | - ---- - -## Distribucion de Story Points - -``` -US-MGN004-001 (Gestion Tenants): ████████████████████████████ 13 SP (28%) -US-MGN004-002 (Configuracion): █████████████████ 8 SP (17%) -US-MGN004-003 (Aislamiento RLS): ████████████████████████████ 13 SP (28%) -US-MGN004-004 (Subscripciones): ████████████████████████████ 13 SP (28%) - ───────────────────────────── - Total: 47 SP -``` - ---- - -## Orden de Implementacion Recomendado - -1. **US-MGN004-003** - Aislamiento de Datos (Base para todo) - - RLS policies - - TenantGuard - - TenantContextMiddleware - - TenantBaseEntity - -2. **US-MGN004-001** - Gestion de Tenants (CRUD basico) - - Entidad Tenant - - TenantsService - - Platform Admin endpoints - -3. **US-MGN004-002** - Configuracion de Tenant - - TenantSettings - - Upload de logos - - Merge con defaults - -4. **US-MGN004-004** - Subscripciones y Limites (Monetizacion) - - Plans, Subscriptions - - LimitGuard, ModuleGuard - - Billing integration - ---- - -## Dependencias con Otros Modulos - -| Modulo | Dependencia | -|--------|-------------| -| MGN-001 Auth | JWT debe incluir tenant_id claim | -| MGN-002 Users | Users deben tener tenant_id | -| MGN-003 RBAC | Roles son per-tenant, permisos de platform admin | - ---- - -## Riesgos Identificados - -| Riesgo | Mitigacion | -|--------|------------| -| RLS mal configurado expone datos | Tests exhaustivos de aislamiento | -| Performance con muchos tenants | Indices compuestos, caching | -| Migracion de datos existentes | Script de migracion con tenant default | -| Complejidad de billing | Integracion con Stripe (proven) | - ---- - -## Definition of Done del Modulo - -- [ ] US-MGN004-001: CRUD de tenants completo -- [ ] US-MGN004-002: Settings funcionando con merge -- [ ] US-MGN004-003: RLS aplicado en TODAS las tablas -- [ ] US-MGN004-003: Tests de aislamiento pasando -- [ ] US-MGN004-004: Planes y limites configurados -- [ ] US-MGN004-004: LimitGuard y ModuleGuard activos -- [ ] Tests unitarios > 80% coverage -- [ ] Tests de aislamiento exhaustivos -- [ ] Security review de RLS aprobado -- [ ] Performance tests con multiples tenants -- [ ] Documentacion Swagger completa - ---- - -## Metricas de Exito - -| Metrica | Objetivo | -|---------|----------| -| Tiempo de respuesta (con RLS) | < 100ms p95 | -| Tests de aislamiento | 100% pass | -| Cobertura de tests | > 80% | -| Vulnerabilidades cross-tenant | 0 | - ---- - -## Historial - -| Version | Fecha | Autor | Cambios | -|---------|-------|-------|---------| -| 1.0 | 2025-12-05 | System | Creacion inicial con 4 US | diff --git a/docs/99-historial/CHANGELOG-REESTRUCTURACION.md b/docs/99-historial/CHANGELOG-REESTRUCTURACION.md new file mode 100644 index 0000000..4bc4d89 --- /dev/null +++ b/docs/99-historial/CHANGELOG-REESTRUCTURACION.md @@ -0,0 +1,292 @@ +# CHANGELOG - Reestructuración de Documentación ERP-CORE + +## Fecha: 2026-01-10 + +--- + +## TAREA 1: PURGA DE DOCUMENTACIÓN LEGACY + +### Resumen +Eliminación del directorio `_legacy_backup` que contenía duplicados exactos de documentación canónica. + +### Métricas +| Métrica | Antes | Después | Diferencia | +|---------|-------|---------|------------| +| Total archivos .md | 840 | 821 | -19 | +| Directorios legacy | 1 | 0 | -1 | +| Tamaño liberado | - | 204 KB | - | + +### Archivos Eliminados (20) + +#### MGN-001 (Auth) - 5 archivos +- `_legacy_backup/MGN-001/BACKLOG-MGN001.md` +- `_legacy_backup/MGN-001/US-MGN001-001.md` +- `_legacy_backup/MGN-001/US-MGN001-002.md` +- `_legacy_backup/MGN-001/US-MGN001-003.md` +- `_legacy_backup/MGN-001/US-MGN001-004.md` + +#### MGN-002 (Users) - 5 archivos +- `_legacy_backup/MGN-002/BACKLOG-MGN002.md` +- `_legacy_backup/MGN-002/US-MGN002-001.md` +- `_legacy_backup/MGN-002/US-MGN002-002.md` +- `_legacy_backup/MGN-002/US-MGN002-003.md` +- `_legacy_backup/MGN-002/US-MGN002-004.md` + +#### MGN-003 (Roles) - 5 archivos +- `_legacy_backup/MGN-003/BACKLOG-MGN003.md` +- `_legacy_backup/MGN-003/US-MGN003-001.md` +- `_legacy_backup/MGN-003/US-MGN003-002.md` +- `_legacy_backup/MGN-003/US-MGN003-003.md` +- `_legacy_backup/MGN-003/US-MGN003-004.md` + +#### MGN-004 (Tenants) - 5 archivos +- `_legacy_backup/MGN-004/BACKLOG-MGN004.md` +- `_legacy_backup/MGN-004/US-MGN004-001.md` +- `_legacy_backup/MGN-004/US-MGN004-002.md` +- `_legacy_backup/MGN-004/US-MGN004-003.md` +- `_legacy_backup/MGN-004/US-MGN004-004.md` + +### Justificación +- **100% duplicación**: Todos los archivos eran idénticos byte a byte a sus equivalentes canónicos +- **Ubicación canónica**: `/docs/01-fase-foundation/MGN-00X-*/historias-usuario/` +- **0 referencias activas**: No existían referencias a estos archivos en ningún documento + +### Verificación Post-Eliminación +- [x] Directorio `_legacy_backup` eliminado +- [x] 0 referencias rotas nuevas (solo historial) +- [x] Archivos canónicos accesibles + +**Estado: COMPLETADO** (2026-01-10) + +--- + +## TAREA 2: CONSOLIDACIÓN DE BACKLOGS + +### Resumen +Eliminación de 4 backlogs duplicados en `/docs/05-user-stories/mgn-00x/` que eran idénticos a los canónicos en `/docs/01-fase-foundation/`. + +### Métricas +| Métrica | Antes | Después | Diferencia | +|---------|-------|---------|------------| +| Total backlogs | 8 | 4 | -4 | +| Ubicaciones | 2 | 1 | -1 | + +### Archivos Eliminados (4) +- `05-user-stories/mgn-001/BACKLOG-MGN-001.md` +- `05-user-stories/mgn-002/BACKLOG-MGN-002.md` +- `05-user-stories/mgn-003/BACKLOG-MGN-003.md` +- `05-user-stories/mgn-004/BACKLOG-MGN-004.md` + +### Archivos Canónicos Mantenidos (4) +- `01-fase-foundation/MGN-001-auth/historias-usuario/BACKLOG-MGN001.md` +- `01-fase-foundation/MGN-002-users/historias-usuario/BACKLOG-MGN002.md` +- `01-fase-foundation/MGN-003-roles/historias-usuario/BACKLOG-MGN003.md` +- `01-fase-foundation/MGN-004-tenants/historias-usuario/BACKLOG-MGN004.md` + +### Justificación +- **100% duplicación**: Contenido idéntico en 891 líneas totales +- **0 referencias activas**: Ningún documento referenciaba los duplicados +- **Ubicación canónica consolidada**: Solo `/docs/01-fase-foundation/` + +**Estado: COMPLETADO** (2026-01-10) + +--- + +## TAREA 3: UNIFICACIÓN DE USER STORIES + +### Resumen +Eliminación de 42 User Stories duplicadas en Foundation y Core Business, manteniendo `/docs/05-user-stories/` como fuente única de verdad (más detallada y completa). + +### Métricas +| Métrica | Antes | Después | Diferencia | +|---------|-------|---------|------------| +| Total archivos .md | 817 | 775 | -42 | +| US en Foundation | 17 | 0 | -17 | +| US en Core Business | 25 | 0 | -25 | +| US en 05-user-stories | 147 | 147 | 0 | +| Ubicaciones de US | 3 | 1 | -2 | + +### Archivos Eliminados (42) + +#### Foundation (17 archivos) +- MGN-001: US-MGN001-001 a US-MGN001-004 (4) +- MGN-002: US-MGN002-001 a US-MGN002-005 (5) +- MGN-003: US-MGN003-001 a US-MGN003-004 (4) +- MGN-004: US-MGN004-001 a US-MGN004-004 (4) + +#### Core Business (25 archivos) +- MGN-005: US-MGN005-001 a US-MGN005-005 (5) +- MGN-006: US-MGN006-001 a US-MGN006-004 (4) +- MGN-007: US-MGN007-001 a US-MGN007-004 (4) +- MGN-008: US-MGN008-001 a US-MGN008-004 (4) +- MGN-009: US-MGN009-001 a US-MGN009-004 (4) +- MGN-010: US-MGN010-001 a US-MGN010-004 (4) + +### Ubicación Canónica Consolidada +`/docs/05-user-stories/mgn-XXX/US-MGN-XXX-00X-00X-nombre.md` +- 147 User Stories consolidadas +- Formato más detallado con: + - Reglas de Negocio explícitas + - Casos de prueba detallados + - Tareas técnicas por subsistema + - Definition of Done completo + +**Estado: COMPLETADO** (2026-01-10) + +--- + +## TAREA 4: CONSOLIDACIÓN DE REQUERIMIENTOS FUNCIONALES + +### Resumen +Eliminación del directorio `/docs/03-requerimientos/` completo que contenía duplicados exactos de los RF canónicos en Foundation y Core Business. + +### Métricas +| Métrica | Antes | Después | Diferencia | +|---------|-------|---------|------------| +| Total archivos .md | 775 | 747 | -28 | +| Directorio eliminado | `/docs/03-requerimientos/` | - | -1 | +| Archivos RF duplicados | 28 | 0 | -28 | +| Ubicaciones de RF | 3 | 2 | -1 | + +### Directorios y Archivos Eliminados +- `/docs/03-requerimientos/RF-auth/` (5 archivos + índice) +- `/docs/03-requerimientos/RF-users/` (5 archivos + índice) +- `/docs/03-requerimientos/RF-rbac/` (4 archivos + índice) +- `/docs/03-requerimientos/RF-tenants/` (4 archivos + índice) +- `/docs/03-requerimientos/RF-catalogs/` (5 archivos + índice) + +### Ubicaciones Canónicas Mantenidas +1. **Foundation/Core Business** (37 RF detallados - para análisis MVP) + - `/docs/01-fase-foundation/MGN-00X-*/requerimientos/` + - `/docs/02-fase-core-business/MGN-00X-*/requerimientos/` + +2. **Modelado** (97 RF - para desarrollo) + - `/docs/04-modelado/requerimientos-funcionales/mgn-*/` + +### Justificación +- **100% duplicación**: Archivos idénticos (MD5 hash coincidente) +- **Estructura redundante**: Misma información reorganizada sin valor adicional + +**Estado: COMPLETADO** (2026-01-10) + +--- + +## TAREA 5: HOMOLOGACIÓN DE NOMENCLATURA + +### Resumen +Análisis de inconsistencias de nomenclatura en archivos y directorios. + +### Inconsistencias Identificadas (32 total) + +| Tipo | Cantidad | Estado | +|------|----------|--------| +| Directorios en minúsculas (mgn-XXX) | 14 | Documentado | +| Archivos BACKLOG sin guiones | 4 | Documentado | +| Archivos ET en minúsculas | 8 | Documentado | +| Índices con plural | 6 | Documentado | + +### Estándares Definidos +``` +DIRECTORIOS: MGN-XXX-nombre (mayúsculas en código) +BACKLOGS: BACKLOG-MGN-XXX.md (con guiones) +ET: ET-[CONTEXTO]-tipo.md (CONTEXTO en mayúsculas) +RF: RF-[CONTEXTO]-XXX.md (CONTEXTO en mayúsculas) +ÍNDICES: INDICE-[TIPO]-[CONTEXTO].md (singular) +``` + +### Decisión +Las inconsistencias son menores (convenciones de estilo) y NO afectan la funcionalidad. Se documentan para implementación progresiva posterior sin riesgo de romper referencias. + +**Estado: DOCUMENTADO** (2026-01-10) - Implementación pendiente como mejora continua + +--- + +## TAREA 6: SINCRONIZACIÓN DOCS-CÓDIGO + +### Resumen +Creación de documentación básica para 8 módulos backend sin MGN-ID formal. + +### Estructura Creada + +**Nuevo directorio:** `/docs/03-fase-vertical/` + +| MGN-ID | Módulo | Servicios | Complejidad | +|--------|--------|-----------|-------------| +| MGN-011 | Sales | 5 | Alta | +| MGN-012 | Purchases | 2 | Media | +| MGN-013 | Inventory | 9 | Muy Alta | +| MGN-014 | HR | 7 | Muy Alta | +| MGN-015 | CRM | 4 | Media | +| MGN-016 | Projects | 3 | Media-Alta | +| MGN-017 | Partners | 2 | Media | +| MGN-018 | Companies | 1 | Baja | + +### Archivos Creados (9) +- `/docs/03-fase-vertical/README.md` +- `/docs/03-fase-vertical/MGN-011-sales/README.md` +- `/docs/03-fase-vertical/MGN-012-purchases/README.md` +- `/docs/03-fase-vertical/MGN-013-inventory/README.md` +- `/docs/03-fase-vertical/MGN-014-hr/README.md` +- `/docs/03-fase-vertical/MGN-015-crm/README.md` +- `/docs/03-fase-vertical/MGN-016-projects/README.md` +- `/docs/03-fase-vertical/MGN-017-partners/README.md` +- `/docs/03-fase-vertical/MGN-018-companies/README.md` + +**Estado: COMPLETADO** (2026-01-10) + +--- + +## RESUMEN EJECUTIVO DE REESTRUCTURACIÓN + +### Métricas Finales + +| Métrica | Antes | Después | Cambio | +|---------|-------|---------|--------| +| Total archivos .md | 840 | 756 | -84 (10%) | +| Directorios legacy | 1 | 0 | -1 | +| Backlogs duplicados | 4 | 0 | -4 | +| User Stories duplicadas | 42 | 0 | -42 | +| RF duplicados | 28 | 0 | -28 | +| Módulos documentados | 10 | 18 | +8 | + +### Tareas Completadas +- [x] TAREA 1: Purga de documentación legacy (-19 archivos) +- [x] TAREA 2: Consolidación de backlogs (-4 archivos) +- [x] TAREA 3: Unificación de user stories (-42 archivos) +- [x] TAREA 4: Consolidación de RF (-28 archivos) +- [x] TAREA 5: Homologación de nomenclatura (documentada) +- [x] TAREA 6: Sincronización docs-código (+9 archivos) +- [x] TAREA 7: Actualización de referencias (_MAP.md, README.md) +- [x] TAREA 8: Validación final + +### Validación Final (2026-01-10) + +| Verificación | Resultado | Estado | +|--------------|-----------|--------| +| Directorio legacy_backup | 0 | PASS | +| Directorio 03-requerimientos | 0 | PASS | +| User Stories en 05-user-stories | 147 | PASS | +| Backlogs canónicos | 4 | PASS | +| Módulos documentados | 18 | PASS | + +### Tareas de Mejora Continua +- [ ] Implementación de renombramientos de nomenclatura (32 archivos) +- [ ] Completar RF/ET/US para módulos MGN-011 a MGN-018 +- [ ] Actualizar referencias en archivos de épicas (12 archivos) + +### Ubicaciones Canónicas Consolidadas + +1. **User Stories:** `/docs/05-user-stories/` (147 archivos) +2. **Backlogs:** `/docs/01-fase-foundation/MGN-XXX/historias-usuario/` (4 archivos) +3. **Requerimientos:** + - Foundation/Core: `/docs/0X-fase-*/MGN-XXX/requerimientos/` (37 archivos) + - Modelado: `/docs/04-modelado/requerimientos-funcionales/` (97 archivos) + +### Directorios Eliminados +- `/docs/05-user-stories/_legacy_backup/` (20 archivos) +- `/docs/03-requerimientos/` (28 archivos) + +--- + +*Reestructuración ejecutada: 2026-01-10* +*Agente: Claude Code - Perfil Documentation Architect* diff --git a/docs/README.md b/docs/README.md index a47dfb3..036154d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -217,9 +217,9 @@ docs/ ├── 00-vision-general/ # Vision del proyecto ├── 01-analisis-referencias/ # Analisis Odoo/Gamilit ├── 02-definicion-modulos/ # Definiciones de modulos -├── 03-requerimientos/ # RF dispersos por tipo -├── 04-modelado/ # ET y DDL dispersos -├── 05-user-stories/ # US dispersas +├── 03-fase-vertical/ # Módulos verticales (MGN-011 a MGN-018) +├── 04-modelado/ # ET y DDL consolidados +├── 05-user-stories/ # US consolidadas (147 archivos) ├── 06-test-plans/ # Planes de prueba ├── 07-devops/ # CI/CD ├── 08-epicas/ # Epicas diff --git a/docs/_MAP.md b/docs/_MAP.md index 2d4bb40..6669211 100644 --- a/docs/_MAP.md +++ b/docs/_MAP.md @@ -1,9 +1,9 @@ # Mapa de Documentacion: erp-core **Proyecto:** erp-core -**Actualizado:** 2026-01-07 -**Generado por:** Backend-Agent + Frontend-Agent -**Version:** 1.0.0 +**Actualizado:** 2026-01-10 +**Generado por:** Backend-Agent + Frontend-Agent + Architecture-Analyst +**Version:** 2.0.0 --- @@ -12,7 +12,10 @@ | Fase | Nombre | Sprints | Estado | |------|--------|---------|--------| | 01 | Foundation | 1-5 | Completado | -| 02 | Core Business | 6-7 | En Progreso | +| 02 | Core Business | 6-13 | Completado | +| 03 | Mobile | 14-15 | Completado | +| 04 | SaaS Platform | - | Planificado | +| 05 | IA Intelligence | - | Planificado | ### Sprints Completados @@ -22,6 +25,9 @@ | Sprint 5 | Security Enhancements | 29 SP | Completado | | Sprint 6 | Catalogs & Settings | 35 SP | Completado | | Sprint 7 | Audit & Notifications | 35 SP | Completado | +| Sprint 8-13 | Reports & Financial | 60 SP | Completado | +| Sprint 14 | Mobile Foundation | 21 SP | Completado | +| Sprint 15 | Mobile Extended + Testing | 10 SP | Completado | --- @@ -30,21 +36,44 @@ ``` docs/ ├── _MAP.md # Este archivo (indice) -├── 00-vision-general/ # Vision general -├── 01-fase-foundation/ # Modulos Fase 1 +├── 00-vision-general/ # Vision general y arquitectura +│ ├── VISION-ERP-CORE.md # Documento principal +│ ├── ARQUITECTURA-SAAS.md # Arquitectura SaaS +│ ├── ARQUITECTURA-IA.md # Arquitectura IA +│ ├── INTEGRACIONES-EXTERNAS.md # Catalogo de integraciones +│ └── STACK-TECNOLOGICO.md # Stack tecnologico +├── 01-fase-foundation/ # Modulos Fase 1 (MGN-001 a MGN-004) │ ├── MGN-001-auth/ # Autenticacion │ ├── MGN-002-users/ # Usuarios │ ├── MGN-003-roles/ # Roles y Permisos │ └── MGN-004-tenants/ # Multi-tenancy -├── 02-fase-core-business/ # Modulos Fase 2 -│ ├── MGN-005-catalogs/ # Catalogos (Sprint 6) -│ ├── MGN-006-settings/ # Settings (Sprint 6) -│ ├── MGN-007-audit/ # Auditoria (Sprint 7) -│ └── MGN-008-notifications/ # Notificaciones (Sprint 7) -├── 03-requerimientos/ # Requerimientos funcionales -├── 04-modelado/ # Modelos de datos -├── 05-user-stories/ # Historias de usuario -└── 97-adr/ # Architecture Decision Records +├── 02-fase-core-business/ # Modulos Fase 2 (MGN-005 a MGN-015) +│ ├── MGN-005-catalogs/ # Catalogos +│ ├── MGN-006-settings/ # Settings +│ ├── MGN-007-audit/ # Auditoria +│ ├── MGN-008-notifications/ # Notificaciones multicanal +│ ├── MGN-009-reports/ # Reports +│ ├── MGN-010-financial/ # Financial +│ ├── MGN-011-inventory/ # Inventario +│ ├── MGN-012-purchasing/ # Compras +│ ├── MGN-013-sales/ # Ventas +│ ├── MGN-014-crm/ # CRM +│ └── MGN-015-projects/ # Proyectos +├── 03-fase-mobile/ # Modulos Mobile +│ └── MOB-001-foundation/ # Mobile Foundation +├── 04-fase-saas/ # Modulos SaaS Platform (MGN-016 a MGN-019) +│ ├── MGN-016-billing/ # Billing con Stripe +│ ├── MGN-017-plans/ # Planes y feature gating +│ ├── MGN-018-webhooks/ # Webhooks outbound +│ └── MGN-019-feature-flags/ # Feature flags +├── 05-fase-ia/ # Modulos IA Intelligence (MGN-020 a MGN-022) +│ ├── MGN-020-ai-integration/ # Gateway LLM (OpenRouter) +│ ├── MGN-021-whatsapp-business/ # WhatsApp Business +│ └── MGN-022-mcp-server/ # MCP Server +├── 06-modelado/ # Modelos de datos +├── 07-user-stories/ # Historias de usuario +├── 97-adr/ # Architecture Decision Records +└── 99-historial/ # Historial de reestructuracion ``` ## Navegacion por Modulos @@ -58,25 +87,107 @@ docs/ | [MGN-003](./01-fase-foundation/MGN-003-roles/_MAP.md) | Roles | 25 | Implementado | | [MGN-004](./01-fase-foundation/MGN-004-tenants/_MAP.md) | Tenants | 35 | Implementado | -### Fase 2: Core Business (En Progreso) +### Fase 2: Core Business (Completada) | Modulo | Nombre | SP | Sprint | Estado | |--------|--------|---:|--------|--------| | [MGN-005](./02-fase-core-business/MGN-005-catalogs/_MAP.md) | Catalogs | 30 | 6 | Implementado | | [MGN-006](./02-fase-core-business/MGN-006-settings/_MAP.md) | Settings | 25 | 6 | Implementado | | [MGN-007](./02-fase-core-business/MGN-007-audit/_MAP.md) | Audit | 30 | 7 | Implementado | -| [MGN-008](./02-fase-core-business/MGN-008-notifications/_MAP.md) | Notifications | 25 | 7 | Parcial | -| [MGN-009](./02-fase-core-business/MGN-009-reports/_MAP.md) | Reports | - | - | Pendiente | -| [MGN-010](./02-fase-core-business/MGN-010-financial/_MAP.md) | Financial | - | - | Pendiente | +| [MGN-008](./02-fase-core-business/MGN-008-notifications/_MAP.md) | Notifications | 25 | 7 | Implementado | +| [MGN-009](./02-fase-core-business/MGN-009-reports/_MAP.md) | Reports | 45 | 8-13 | Implementado | +| [MGN-010](./02-fase-core-business/MGN-010-financial/_MAP.md) | Financial | 15 | 8-13 | Implementado | + +### Fase 3: Mobile (Completada) + +| Modulo | Nombre | SP | Sprint | Estado | +|--------|--------|---:|--------|--------| +| [MOB-001](./03-fase-mobile/MOB-001-foundation/_MAP.md) | Mobile Foundation | 31 | 14-15 | Implementado | + +### Fase 4: SaaS Platform (Planificada) + +| Modulo | Nombre | Descripcion | Estado | +|--------|--------|-------------|--------| +| [MGN-016](./04-fase-saas/MGN-016-billing/) | Billing | Suscripciones y pagos (Stripe) | Planificado | +| [MGN-017](./04-fase-saas/MGN-017-plans/) | Plans | Planes, limites y feature gating | Planificado | +| [MGN-018](./04-fase-saas/MGN-018-webhooks/) | Webhooks | Webhooks outbound con HMAC | Planificado | +| [MGN-019](./04-fase-saas/MGN-019-feature-flags/) | Feature Flags | Feature flags por tenant/usuario | Planificado | + +### Fase 5: IA Intelligence (Planificada) + +| Modulo | Nombre | Descripcion | Estado | +|--------|--------|-------------|--------| +| [MGN-020](./05-fase-ia/MGN-020-ai-integration/) | AI Integration | Gateway LLM (OpenRouter) | Planificado | +| [MGN-021](./05-fase-ia/MGN-021-whatsapp-business/) | WhatsApp Business | WhatsApp con IA conversacional | Planificado | +| [MGN-022](./05-fase-ia/MGN-022-mcp-server/) | MCP Server | Model Context Protocol Server | Planificado | --- ## Estadisticas -- **Total Story Points:** 219 SP (completados) -- **Total Tests:** 647 passing -- **Total Tablas DB:** 179 -- **Total Endpoints:** 80+ +- **Total Modulos:** 22 (MGN-001 a MGN-022 + MOB-001) +- **Total Story Points:** 310+ SP (Fases 1-3) +- **Total Tests:** 700+ passing (Backend, Frontend, Mobile) +- **Total Tablas DB:** 191 (Core) + Pending (SaaS/IA) +- **Total Endpoints:** 80+ (documentados en OpenAPI) +- **API Documentation:** Swagger UI `/api/v1/docs` +- **Frontend Dark Mode:** Implementado +- **Mobile App:** Expo 51 con 6 screens, offline, notifications, biometrics +- **CI/CD:** GitHub Actions (8 jobs) + +### Cobertura por Fase + +| Fase | Modulos | Estado | +|------|---------|--------| +| Foundation | MGN-001 a MGN-004 | Implementado | +| Core Business | MGN-005 a MGN-015 | En desarrollo | +| Mobile | MOB-001 | Implementado | +| SaaS Platform | MGN-016 a MGN-019 | Planificado | +| IA Intelligence | MGN-020 a MGN-022 | Planificado | + +--- + +## API Documentation (DOC-001) + +| Recurso | Ubicacion | +|---------|-----------| +| OpenAPI Spec | `backend/src/docs/openapi.yaml` | +| Swagger UI | http://localhost:3000/api/v1/docs | +| Swagger Config | `backend/src/config/swagger.config.ts` | + +**Cobertura:** 18 tags, 12 schemas, 80+ endpoints documentados. + +--- + +## Frontend Components (FE-003) + +### Atoms +| Componente | Descripcion | Dark Mode | +|------------|-------------|-----------| +| Avatar | Avatar de usuario | Si | +| Badge | Badges/etiquetas | Si | +| Button | Botones (primary, secondary, outline, danger) | Si | +| Input | Inputs de formulario | Si | +| Label | Labels de formulario | Si | +| Spinner | Indicador de carga | Si | +| ThemePreview | Preview de temas | Si | +| **ThemeSelector** | Selector de tema (icon/buttons/dropdown) | Si | +| Tooltip | Tooltips informativos | Si | + +### Providers +| Provider | Descripcion | +|----------|-------------| +| ThemeProvider | Gestiona tema (light/dark/system) | +| AppProviders | Wrapper con todos los providers | + +--- + +## CHANGELOGs + +| Fecha | Archivo | Descripcion | +|-------|---------|-------------| +| 2026-01-04 | [CHANGELOG-TYPEORM-FIXES](../backend/docs/CHANGELOG-2026-01-04-TYPEORM-FIXES.md) | TypeORM migration fixes | +| 2026-01-07 | [CHANGELOG-DOC001-FE003](../backend/docs/CHANGELOG-2026-01-07-DOC001-FE003.md) | API Docs Swagger + Dark Mode UI | --- @@ -95,5 +206,19 @@ docs/ --- -**Ultima actualizacion:** 2026-01-07 +## Vision General + +Los documentos de vision y arquitectura estan en `00-vision-general/`: + +| Documento | Descripcion | +|-----------|-------------| +| [VISION-ERP-CORE](./00-vision-general/VISION-ERP-CORE.md) | Vision general, alcances Core/SaaS/IA | +| [ARQUITECTURA-SAAS](./00-vision-general/ARQUITECTURA-SAAS.md) | Multi-tenancy RLS, Billing, Plans | +| [ARQUITECTURA-IA](./00-vision-general/ARQUITECTURA-IA.md) | LLM Gateway, MCP Server, WhatsApp | +| [INTEGRACIONES-EXTERNAS](./00-vision-general/INTEGRACIONES-EXTERNAS.md) | Stripe, SendGrid, OpenRouter, S3 | +| [STACK-TECNOLOGICO](./00-vision-general/STACK-TECNOLOGICO.md) | Node.js, React, PostgreSQL, Redis | + +--- + +**Ultima actualizacion:** 2026-01-10 (v2.0 - SaaS/IA Integration) **Metodologia:** NEXUS v3.4 + SIMCO diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 54725f8..69ffa78 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "zustand": "^5.0.1" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@tailwindcss/forms": "^0.5.9", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", @@ -1447,6 +1448,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -4992,7 +5009,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -6564,6 +6580,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8419,7 +8482,6 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } diff --git a/frontend/package.json b/frontend/package.json index 3a52586..2cad028 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,14 @@ "lint:fix": "eslint . --ext ts,tsx --fix", "test": "vitest", "test:ui": "vitest --ui", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed", + "test:e2e:debug": "playwright test --debug", + "test:e2e:report": "playwright show-report", + "lighthouse": "lhci autorun", + "build:analyze": "vite build && du -sh dist/assets/*.js | sort -h" }, "dependencies": { "@hookform/resolvers": "^3.9.1", @@ -34,6 +41,7 @@ "zustand": "^5.0.1" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@tailwindcss/forms": "^0.5.9", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", diff --git a/frontend/src/app/layouts/DashboardLayout.tsx b/frontend/src/app/layouts/DashboardLayout.tsx index 61f08eb..9013a0d 100644 --- a/frontend/src/app/layouts/DashboardLayout.tsx +++ b/frontend/src/app/layouts/DashboardLayout.tsx @@ -20,6 +20,7 @@ import { Key, LayoutDashboard, BarChart3, + Database, } from 'lucide-react'; import { cn } from '@utils/cn'; import { useUIStore } from '@stores/useUIStore'; @@ -27,6 +28,7 @@ import { useAuthStore } from '@stores/useAuthStore'; import { useIsMobile } from '@hooks/useMediaQuery'; import { NotificationBell } from '@features/notifications/components'; import { useNotificationSocket } from '@features/notifications/hooks'; +import { ThemeSelector } from '@components/atoms/ThemeSelector'; interface DashboardLayoutProps { children: ReactNode; @@ -37,6 +39,7 @@ const navigation = [ { name: 'Usuarios', href: '/users', icon: Users }, { name: 'Empresas', href: '/companies', icon: Building2 }, { name: 'Partners', href: '/partners', icon: Users2 }, + { name: 'Catalogos', href: '/catalogs', icon: Database }, { name: 'Inventario', href: '/inventory', icon: Package }, { name: 'Ventas', href: '/sales', icon: ShoppingCart }, { name: 'Compras', href: '/purchases', icon: ShoppingCart }, @@ -76,11 +79,11 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { }, [location.pathname, isMobile, setSidebarOpen]); return ( -
+
{/* Mobile sidebar backdrop */} {isMobile && sidebarOpen && (
setSidebarOpen(false)} /> )} @@ -88,7 +91,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { {/* Sidebar */}