[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0 + cambios backend
Some checks failed
ERP Core CI / Backend Lint (push) Has been cancelled
ERP Core CI / Backend Unit Tests (push) Has been cancelled
ERP Core CI / Backend Integration Tests (push) Has been cancelled
ERP Core CI / Frontend Lint (push) Has been cancelled
ERP Core CI / Frontend Unit Tests (push) Has been cancelled
ERP Core CI / Frontend E2E Tests (push) Has been cancelled
ERP Core CI / Database DDL Validation (push) Has been cancelled
ERP Core CI / Backend Build (push) Has been cancelled
ERP Core CI / Frontend Build (push) Has been cancelled
ERP Core CI / CI Success (push) Has been cancelled
Performance Tests / Lighthouse CI (push) Has been cancelled
Performance Tests / Bundle Size Analysis (push) Has been cancelled
Performance Tests / k6 Load Tests (push) Has been cancelled
Performance Tests / Performance Summary (push) Has been cancelled
Some checks failed
ERP Core CI / Backend Lint (push) Has been cancelled
ERP Core CI / Backend Unit Tests (push) Has been cancelled
ERP Core CI / Backend Integration Tests (push) Has been cancelled
ERP Core CI / Frontend Lint (push) Has been cancelled
ERP Core CI / Frontend Unit Tests (push) Has been cancelled
ERP Core CI / Frontend E2E Tests (push) Has been cancelled
ERP Core CI / Database DDL Validation (push) Has been cancelled
ERP Core CI / Backend Build (push) Has been cancelled
ERP Core CI / Frontend Build (push) Has been cancelled
ERP Core CI / CI Success (push) Has been cancelled
Performance Tests / Lighthouse CI (push) Has been cancelled
Performance Tests / Bundle Size Analysis (push) Has been cancelled
Performance Tests / k6 Load Tests (push) Has been cancelled
Performance Tests / Performance Summary (push) Has been cancelled
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones en modulos CRM y OpenAPI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f95b8d4577
commit
0086695b4c
381
.github/workflows/ci.yml
vendored
Normal file
381
.github/workflows/ci.yml
vendored
Normal file
@ -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!"
|
||||||
231
.github/workflows/performance.yml
vendored
Normal file
231
.github/workflows/performance.yml
vendored
Normal file
@ -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
|
||||||
2150
backend/package-lock.json
generated
2150
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -25,8 +25,10 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
|
"nodemailer": "^7.0.12",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
"puppeteer": "^22.15.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"socket.io": "^4.7.4",
|
"socket.io": "^4.7.4",
|
||||||
@ -48,6 +50,7 @@
|
|||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@types/nodemailer": "^7.0.4",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
"@types/socket.io": "^3.0.0",
|
"@types/socket.io": "^3.0.0",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ import { z } from 'zod';
|
|||||||
import { leadsService, CreateLeadDto, UpdateLeadDto, LeadFilters } from './leads.service.js';
|
import { leadsService, CreateLeadDto, UpdateLeadDto, LeadFilters } from './leads.service.js';
|
||||||
import { opportunitiesService, CreateOpportunityDto, UpdateOpportunityDto, OpportunityFilters } from './opportunities.service.js';
|
import { opportunitiesService, CreateOpportunityDto, UpdateOpportunityDto, OpportunityFilters } from './opportunities.service.js';
|
||||||
import { stagesService, CreateLeadStageDto, UpdateLeadStageDto, CreateOpportunityStageDto, UpdateOpportunityStageDto, CreateLostReasonDto, UpdateLostReasonDto } from './stages.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 { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js';
|
||||||
import { ValidationError } from '../../shared/errors/index.js';
|
import { ValidationError } from '../../shared/errors/index.js';
|
||||||
|
|
||||||
@ -677,6 +678,94 @@ class CrmController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== TAGS ==========
|
||||||
|
|
||||||
|
async getTags(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
export const crmController = new CrmController();
|
||||||
|
|||||||
@ -123,4 +123,22 @@ router.delete('/lost-reasons/:id', requireRoles('admin', 'super_admin'), (req, r
|
|||||||
crmController.deleteLostReason(req, res, next)
|
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;
|
export default router;
|
||||||
|
|||||||
@ -21,3 +21,15 @@ export { Tax, TaxType } from './tax.entity.js';
|
|||||||
// Fiscal period entities
|
// Fiscal period entities
|
||||||
export { FiscalYear, FiscalPeriodStatus } from './fiscal-year.entity.js';
|
export { FiscalYear, FiscalPeriodStatus } from './fiscal-year.entity.js';
|
||||||
export { FiscalPeriod } from './fiscal-period.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';
|
||||||
|
|||||||
@ -6,6 +6,10 @@ import { journalEntriesService, CreateJournalEntryDto, UpdateJournalEntryDto, Jo
|
|||||||
import { invoicesService, CreateInvoiceDto, UpdateInvoiceDto, CreateInvoiceLineDto, UpdateInvoiceLineDto, InvoiceFilters } from './invoices.service.js';
|
import { invoicesService, CreateInvoiceDto, UpdateInvoiceDto, CreateInvoiceLineDto, UpdateInvoiceLineDto, InvoiceFilters } from './invoices.service.js';
|
||||||
import { paymentsService, CreatePaymentDto, UpdatePaymentDto, ReconcileDto, PaymentFilters } from './payments.service.js';
|
import { paymentsService, CreatePaymentDto, UpdatePaymentDto, ReconcileDto, PaymentFilters } from './payments.service.js';
|
||||||
import { taxesService, CreateTaxDto, UpdateTaxDto, TaxFilters } from './taxes.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 { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js';
|
||||||
import { ValidationError } from '../../shared/errors/index.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),
|
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 {
|
class FinancialController {
|
||||||
// ========== ACCOUNT TYPES ==========
|
// ========== ACCOUNT TYPES ==========
|
||||||
async getAccountTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
async getAccountTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
||||||
@ -777,6 +909,430 @@ class FinancialController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== INCOTERMS (read-only) ==========
|
||||||
|
async getIncoterms(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
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<IncotermFilters>(queryResult.data as Record<string, unknown>);
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<PaymentMethodFilters>(queryResult.data as Record<string, unknown>);
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<ReconcileModelFilters>(queryResult.data as Record<string, unknown>);
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
export const financialController = new FinancialController();
|
||||||
|
|||||||
@ -147,4 +147,74 @@ router.delete('/taxes/:id', requireRoles('admin', 'super_admin'), (req, res, nex
|
|||||||
financialController.deleteTax(req, res, next)
|
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;
|
export default router;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { hrController } from './hr.controller.js';
|
import { hrController } from './hr.controller.js';
|
||||||
|
import { hrExtendedController } from './hr-extended.controller.js';
|
||||||
import { authenticate, requireRoles } from '../../shared/middleware/auth.middleware.js';
|
import { authenticate, requireRoles } from '../../shared/middleware/auth.middleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -149,4 +150,187 @@ router.delete('/leaves/:id', requireRoles('admin', 'super_admin'), (req, res, ne
|
|||||||
hrController.deleteLeave(req, res, next)
|
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;
|
export default router;
|
||||||
|
|||||||
@ -2,5 +2,9 @@ export * from './employees.service.js';
|
|||||||
export * from './departments.service.js';
|
export * from './departments.service.js';
|
||||||
export * from './contracts.service.js';
|
export * from './contracts.service.js';
|
||||||
export * from './leaves.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.controller.js';
|
||||||
|
export * from './hr-extended.controller.js';
|
||||||
export { default as hrRoutes } from './hr.routes.js';
|
export { default as hrRoutes } from './hr.routes.js';
|
||||||
|
|||||||
@ -9,3 +9,4 @@ export * from './stock-move.entity.js';
|
|||||||
export * from './inventory-adjustment.entity.js';
|
export * from './inventory-adjustment.entity.js';
|
||||||
export * from './inventory-adjustment-line.entity.js';
|
export * from './inventory-adjustment-line.entity.js';
|
||||||
export * from './stock-valuation-layer.entity.js';
|
export * from './stock-valuation-layer.entity.js';
|
||||||
|
export * from './package-type.entity.js';
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { locationsService, CreateLocationDto, UpdateLocationDto, LocationFilters
|
|||||||
import { pickingsService, CreatePickingDto, PickingFilters } from './pickings.service.js';
|
import { pickingsService, CreatePickingDto, PickingFilters } from './pickings.service.js';
|
||||||
import { lotsService, CreateLotDto, UpdateLotDto, LotFilters } from './lots.service.js';
|
import { lotsService, CreateLotDto, UpdateLotDto, LotFilters } from './lots.service.js';
|
||||||
import { adjustmentsService, CreateAdjustmentDto, UpdateAdjustmentDto, AdjustmentLineDto, UpdateAdjustmentLineDto, AdjustmentFilters } from './adjustments.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 { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js';
|
||||||
import { ValidationError } from '../../shared/errors/index.js';
|
import { ValidationError } from '../../shared/errors/index.js';
|
||||||
|
|
||||||
@ -931,6 +932,123 @@ class InventoryController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== PACKAGE TYPES ==========
|
||||||
|
async getPackageTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
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<PackageTypeFilters>(queryResult.data as Record<string, unknown>);
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<CreatePackageTypeDto>(parseResult.data as Record<string, unknown>);
|
||||||
|
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<void> {
|
||||||
|
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<UpdatePackageTypeDto>(parseResult.data as Record<string, unknown>);
|
||||||
|
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<void> {
|
||||||
|
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();
|
export const inventoryController = new InventoryController();
|
||||||
|
|||||||
@ -171,4 +171,21 @@ router.post('/valuation/consume', requireRoles('admin', 'manager', 'super_admin'
|
|||||||
valuationController.consumeFifo(req, res, next)
|
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;
|
export default router;
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import { Response, NextFunction } from 'express';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AuthenticatedRequest } from '../../shared/types/index.js';
|
import { AuthenticatedRequest } from '../../shared/types/index.js';
|
||||||
import { reportsService } from './reports.service.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
|
// VALIDATION SCHEMAS
|
||||||
@ -60,6 +63,13 @@ const generalLedgerSchema = z.object({
|
|||||||
date_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
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
|
// CONTROLLER
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -429,6 +439,202 @@ class ReportsController {
|
|||||||
next(error);
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
export const reportsController = new ReportsController();
|
||||||
|
|||||||
@ -16,11 +16,25 @@ router.get('/quick/trial-balance',
|
|||||||
(req, res, next) => reportsController.getTrialBalance(req, res, next)
|
(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',
|
router.get('/quick/general-ledger',
|
||||||
requireRoles('admin', 'manager', 'accountant', 'super_admin'),
|
requireRoles('admin', 'manager', 'accountant', 'super_admin'),
|
||||||
(req, res, next) => reportsController.getGeneralLedger(req, res, next)
|
(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
|
// DEFINITIONS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -65,6 +79,12 @@ router.get('/executions/:id',
|
|||||||
(req, res, next) => reportsController.findExecutionById(req, res, next)
|
(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
|
// SCHEDULES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@ -459,12 +459,21 @@ class OrdersService {
|
|||||||
values.push(dto.analytic_account_id);
|
values.push(dto.analytic_account_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate amounts
|
// Recalculate amounts using taxesService
|
||||||
const subtotal = quantity * priceUnit;
|
const taxIds = dto.tax_ids ?? existingLine.tax_ids;
|
||||||
const discountAmount = subtotal * discount / 100;
|
const taxResult = await taxesService.calculateTaxes(
|
||||||
const amountUntaxed = subtotal - discountAmount;
|
{
|
||||||
const amountTax = 0; // TODO: Calculate taxes
|
quantity,
|
||||||
const amountTotal = amountUntaxed + amountTax;
|
priceUnit,
|
||||||
|
discount,
|
||||||
|
taxIds,
|
||||||
|
},
|
||||||
|
tenantId,
|
||||||
|
'sales'
|
||||||
|
);
|
||||||
|
const amountUntaxed = taxResult.amountUntaxed;
|
||||||
|
const amountTax = taxResult.amountTax;
|
||||||
|
const amountTotal = taxResult.amountTotal;
|
||||||
|
|
||||||
updateFields.push(`amount_untaxed = $${paramIndex++}`);
|
updateFields.push(`amount_untaxed = $${paramIndex++}`);
|
||||||
values.push(amountUntaxed);
|
values.push(amountUntaxed);
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { query, queryOne, getClient } from '../../config/database.js';
|
import { query, queryOne, getClient } from '../../config/database.js';
|
||||||
import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js';
|
import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js';
|
||||||
import { taxesService } from '../financial/taxes.service.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 {
|
export interface QuotationLine {
|
||||||
id: string;
|
id: string;
|
||||||
@ -409,12 +411,21 @@ class QuotationsService {
|
|||||||
values.push(dto.tax_ids);
|
values.push(dto.tax_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate amounts
|
// Recalculate amounts using taxesService
|
||||||
const subtotal = quantity * priceUnit;
|
const taxIds = dto.tax_ids ?? existingLine.tax_ids;
|
||||||
const discountAmount = subtotal * discount / 100;
|
const taxResult = await taxesService.calculateTaxes(
|
||||||
const amountUntaxed = subtotal - discountAmount;
|
{
|
||||||
const amountTax = 0; // TODO: Calculate taxes
|
quantity,
|
||||||
const amountTotal = amountUntaxed + amountTax;
|
priceUnit,
|
||||||
|
discount,
|
||||||
|
taxIds,
|
||||||
|
},
|
||||||
|
tenantId,
|
||||||
|
'sales'
|
||||||
|
);
|
||||||
|
const amountUntaxed = taxResult.amountUntaxed;
|
||||||
|
const amountTax = taxResult.amountTax;
|
||||||
|
const amountTotal = taxResult.amountTotal;
|
||||||
|
|
||||||
updateFields.push(`amount_untaxed = $${paramIndex++}`);
|
updateFields.push(`amount_untaxed = $${paramIndex++}`);
|
||||||
values.push(amountUntaxed);
|
values.push(amountUntaxed);
|
||||||
@ -475,11 +486,257 @@ class QuotationsService {
|
|||||||
[userId, id, tenantId]
|
[userId, id, tenantId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Send email notification
|
// Send email notification to partner
|
||||||
|
await this.sendQuotationEmail(quotation, tenantId);
|
||||||
|
|
||||||
return this.findById(id, tenantId);
|
return this.findById(id, tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send quotation email to partner
|
||||||
|
*/
|
||||||
|
private async sendQuotationEmail(quotation: Quotation, tenantId: string): Promise<void> {
|
||||||
|
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 => `
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #e5e7eb;">${line.description}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #e5e7eb; text-align: center;">${line.quantity}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #e5e7eb; text-align: right;">${currencyCode} ${line.price_unit.toLocaleString()}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #e5e7eb; text-align: right;">${currencyCode} ${line.amount_total.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Cotización ${quotationName}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #2563eb;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 22px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
.quote-info {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.quote-info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.quote-info-label {
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.quote-info-value {
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
th:last-child, th:nth-child(3), th:nth-child(2) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
th:nth-child(2) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.total-row {
|
||||||
|
background-color: #2563eb;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.total-row td {
|
||||||
|
padding: 14px 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.validity {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
border-left: 4px solid #f59e0b;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.contact-link {
|
||||||
|
color: #2563eb;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">${companyName}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Cotización ${quotationName}</h1>
|
||||||
|
|
||||||
|
<p>Estimado/a <strong>${partnerName}</strong>,</p>
|
||||||
|
|
||||||
|
<p>Le enviamos nuestra cotización según lo solicitado. A continuación encontrará el detalle de los productos y/o servicios cotizados:</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Descripción</th>
|
||||||
|
<th>Cantidad</th>
|
||||||
|
<th>Precio Unit.</th>
|
||||||
|
<th>Subtotal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${linesHtml}
|
||||||
|
<tr class="total-row">
|
||||||
|
<td colspan="3" style="text-align: right;">TOTAL:</td>
|
||||||
|
<td style="text-align: right;">${currencyCode} ${amountTotal.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="validity">
|
||||||
|
<strong>Vigencia:</strong> Esta cotización es válida hasta el ${new Date(validityDate).toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric' })}.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Si tiene alguna pregunta o desea proceder con el pedido, no dude en contactarnos.</p>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Atentamente,<br><strong>${companyName}</strong></p>
|
||||||
|
<p>Para cualquier consulta, contáctenos en <a href="mailto:${supportEmail}" class="contact-link">${supportEmail}</a></p>
|
||||||
|
<p>© ${new Date().getFullYear()} ${companyName}. Todos los derechos reservados.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
async confirm(id: string, tenantId: string, userId: string): Promise<{ quotation: Quotation; orderId: string }> {
|
async confirm(id: string, tenantId: string, userId: string): Promise<{ quotation: Quotation; orderId: string }> {
|
||||||
const quotation = await this.findById(id, tenantId);
|
const quotation = await this.findById(id, tenantId);
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import jwt from 'jsonwebtoken';
|
|||||||
import { config } from '../../config/index.js';
|
import { config } from '../../config/index.js';
|
||||||
import { AuthenticatedRequest, JwtPayload, UnauthorizedError, ForbiddenError } from '../types/index.js';
|
import { AuthenticatedRequest, JwtPayload, UnauthorizedError, ForbiddenError } from '../types/index.js';
|
||||||
import { logger } from '../utils/logger.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
|
// Re-export AuthenticatedRequest for convenience
|
||||||
export { AuthenticatedRequest } from '../types/index.js';
|
export { AuthenticatedRequest } from '../types/index.js';
|
||||||
@ -73,15 +75,63 @@ export function requirePermission(resource: string, action: string) {
|
|||||||
throw new UnauthorizedError('Usuario no autenticado');
|
throw new UnauthorizedError('Usuario no autenticado');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { userId, tenantId, roles } = req.user;
|
||||||
|
|
||||||
// Superusers bypass permission checks
|
// Superusers bypass permission checks
|
||||||
if (req.user.roles.includes('super_admin')) {
|
if (roles.includes('super_admin')) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check permission in database
|
// Build permission key for cache lookup
|
||||||
// For now, we'll implement this when we have the permission checking service
|
const permissionKey = `${resource}:${action}`;
|
||||||
logger.debug('Permission check', {
|
|
||||||
userId: req.user.userId,
|
// 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,
|
resource,
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,3 +6,9 @@ export {
|
|||||||
BaseServiceConfig,
|
BaseServiceConfig,
|
||||||
} from './base.service.js';
|
} from './base.service.js';
|
||||||
export { emailService, EmailOptions, EmailResult } from './email.service.js';
|
export { emailService, EmailOptions, EmailResult } from './email.service.js';
|
||||||
|
export {
|
||||||
|
cacheService,
|
||||||
|
cacheKeys,
|
||||||
|
cacheTTL,
|
||||||
|
CacheOptions,
|
||||||
|
} from './cache.service.js';
|
||||||
|
|||||||
793
docs/00-vision-general/ARQUITECTURA-IA.md
Normal file
793
docs/00-vision-general/ARQUITECTURA-IA.md
Normal file
@ -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<Response> {
|
||||||
|
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*
|
||||||
558
docs/00-vision-general/ARQUITECTURA-SAAS.md
Normal file
558
docs/00-vision-general/ARQUITECTURA-SAAS.md
Normal file
@ -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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
// 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*
|
||||||
448
docs/00-vision-general/INTEGRACIONES-EXTERNAS.md
Normal file
448
docs/00-vision-general/INTEGRACIONES-EXTERNAS.md
Normal file
@ -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*
|
||||||
314
docs/00-vision-general/STACK-TECNOLOGICO.md
Normal file
314
docs/00-vision-general/STACK-TECNOLOGICO.md
Normal file
@ -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 <repo-url> 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*
|
||||||
@ -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
|
# Vision General: ERP Core
|
||||||
|
|
||||||
## Resumen Ejecutivo
|
## 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.
|
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
|
## Proposito
|
||||||
@ -19,6 +49,11 @@ Desarrollar ERPs verticales desde cero es costoso y repetitivo. El 60-70% de la
|
|||||||
- Ventas y compras
|
- Ventas y compras
|
||||||
- Contabilidad basica
|
- Contabilidad basica
|
||||||
|
|
||||||
|
**Ademas, las plataformas modernas requieren:**
|
||||||
|
- Modelo de negocio SaaS con suscripciones
|
||||||
|
- Inteligencia artificial integrada
|
||||||
|
- Comunicacion omnicanal con clientes
|
||||||
|
|
||||||
### Solucion
|
### Solucion
|
||||||
|
|
||||||
ERP Core provee esta funcionalidad comun de forma:
|
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
|
- **Extensible:** Las verticales pueden extender sin modificar
|
||||||
- **Multi-tenant:** Aislamiento por tenant desde el diseno
|
- **Multi-tenant:** Aislamiento por tenant desde el diseno
|
||||||
- **Documentado:** Documentacion antes de desarrollo
|
- **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
|
1. Completar modulos core: Auth, Users, Roles, Tenants
|
||||||
2. Implementar Partners y Products
|
2. Implementar Partners y Products
|
||||||
3. Establecer patrones de extension para verticales
|
3. Establecer patrones de extension para verticales
|
||||||
|
4. **Integrar Billing y Plans (Stripe)**
|
||||||
|
5. **Implementar Feature Flags**
|
||||||
|
|
||||||
### Mediano Plazo (6 meses)
|
### Mediano Plazo (6 meses)
|
||||||
1. Completar Sales, Purchases, Inventory
|
1. Completar Sales, Purchases, Inventory
|
||||||
2. Implementar Financial basico
|
2. Implementar Financial basico
|
||||||
3. Primera vertical (Construccion) usando el core
|
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)
|
### Largo Plazo (12 meses)
|
||||||
1. Todas las verticales usando el core
|
1. Todas las verticales usando el core
|
||||||
2. SaaS layer para autocontratacion
|
2. **SaaS layer completo para autocontratacion**
|
||||||
3. Marketplace de extensiones
|
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
|
### Modelo de Capas
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
+------------------------------------------------------------------+
|
||||||
│ FRONTEND │
|
| CLIENTES |
|
||||||
│ React 18 + TypeScript + Tailwind + Zustand │
|
| +----------+ +----------+ +----------+ +----------+ |
|
||||||
├─────────────────────────────────────────────────────────────┤
|
| | Web App | |Mobile App| | WhatsApp | | API Rest | |
|
||||||
│ API REST │
|
| | React 18 | | Expo 51 | | Meta API | | Swagger | |
|
||||||
│ Express.js + TypeScript + Swagger │
|
| +----------+ +----------+ +----------+ +----------+ |
|
||||||
├─────────────────────────────────────────────────────────────┤
|
+------------------------------------------------------------------+
|
||||||
│ BACKEND │
|
| CAPA IA |
|
||||||
│ Modulos: Auth | Users | Partners | Products | Sales... │
|
| +----------------------------------------------------------+ |
|
||||||
│ Services + Controllers + DTOs + Entities │
|
| | MCP SERVER | |
|
||||||
├─────────────────────────────────────────────────────────────┤
|
| | (Model Context Protocol - Herramientas de Negocio) | |
|
||||||
│ DATABASE │
|
| +----------------------------------------------------------+ |
|
||||||
│ PostgreSQL 15+ con RLS (Row-Level Security) │
|
| | LLM GATEWAY (OpenRouter) | |
|
||||||
│ Schemas: core_auth | core_partners | core_products... │
|
| | 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
|
### Modelo de Extension
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
+------------------------------------------------------------------+
|
||||||
│ ERP CORE │
|
| ERP CORE |
|
||||||
│ Modulos Genericos (MGN-001 a MGN-015) │
|
| Modulos Genericos (MGN-001 a MGN-022) |
|
||||||
│ 60-70% funcionalidad comun │
|
| 60-70% funcionalidad comun |
|
||||||
└────────────────────────┬────────────────────────────────────┘
|
| |
|
||||||
│ EXTIENDE
|
| +-- Core Business: auth, users, tenants, catalog, inventory |
|
||||||
┌────────────────┼────────────────┐
|
| +-- SaaS Layer: billing, plans, webhooks, feature-flags |
|
||||||
↓ ↓ ↓
|
| +-- IA Layer: ai-integration, whatsapp, mcp-server |
|
||||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
+------------------------------------------------------------------+
|
||||||
│ Construccion │ │Vidrio Templado│ │ Retail │
|
| EXTIENDE
|
||||||
│ (MAI-*) │ │ (MVT-*) │ │ (MRT-*) │
|
+----------------+----------------+
|
||||||
│ 30-40% extra │ │ 30-40% extra │ │ 30-40% extra │
|
| | |
|
||||||
└───────────────┘ └───────────────┘ └───────────────┘
|
+---------------+ +---------------+ +---------------+
|
||||||
|
| Construccion | |Vidrio Templado| | Retail |
|
||||||
|
| (MAI-*) | | (MVT-*) | | (MRT-*) |
|
||||||
|
| 30-40% extra | | 30-40% extra | | 30-40% extra |
|
||||||
|
+---------------+ +---------------+ +---------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Modulos Core (MGN-*)
|
## Modulos Core (MGN-*)
|
||||||
|
|
||||||
|
### Fase Foundation (P0)
|
||||||
|
|
||||||
| Codigo | Modulo | Descripcion | Prioridad | Estado |
|
| Codigo | Modulo | Descripcion | Prioridad | Estado |
|
||||||
|--------|--------|-------------|-----------|--------|
|
|--------|--------|-------------|-----------|--------|
|
||||||
| MGN-001 | auth | Autenticacion JWT, OAuth, sessions | P0 | En desarrollo |
|
| MGN-001 | auth | Autenticacion JWT, OAuth, sessions | P0 | En desarrollo |
|
||||||
| MGN-002 | users | Gestion de usuarios CRUD | P0 | En desarrollo |
|
| MGN-002 | users | Gestion de usuarios CRUD | P0 | En desarrollo |
|
||||||
| MGN-003 | roles | Roles y permisos (RBAC) | P0 | Planificado |
|
| 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-005 | catalogs | Catalogos maestros genericos | P1 | Planificado |
|
||||||
| MGN-006 | settings | Configuracion del sistema | P1 | Planificado |
|
| MGN-006 | settings | Configuracion del sistema | P1 | Planificado |
|
||||||
| MGN-007 | audit | Auditoria y logs | 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-009 | reports | Reportes genericos | P2 | Planificado |
|
||||||
| MGN-010 | financial | Contabilidad basica | P1 | Planificado |
|
| MGN-010 | financial | Contabilidad basica | P1 | Planificado |
|
||||||
| MGN-011 | inventory | Inventario basico | 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-014 | crm | CRM basico | P2 | Planificado |
|
||||||
| MGN-015 | projects | Proyectos genericos | 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
|
## Stack Tecnologico
|
||||||
|
|
||||||
|
> Ver detalle completo en [STACK-TECNOLOGICO.md](STACK-TECNOLOGICO.md)
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
| Tecnologia | Version | Proposito |
|
| Tecnologia | Version | Proposito |
|
||||||
|------------|---------|-----------|
|
|------------|---------|-----------|
|
||||||
| Node.js | 20+ | Runtime |
|
| Node.js | 20+ | Runtime |
|
||||||
| Express.js | 4.x | Framework HTTP |
|
| Express.js | 4.x | Framework HTTP |
|
||||||
| TypeScript | 5.3+ | Lenguaje |
|
| TypeScript | 5.3+ | Lenguaje |
|
||||||
| TypeORM | 0.3.17 | ORM |
|
| TypeORM | 0.3.17 | ORM |
|
||||||
| JWT + bcryptjs | - | Autenticacion |
|
| BullMQ | 5.x | Colas asincronas |
|
||||||
| Zod, class-validator | - | Validacion |
|
| Socket.io | 4.x | WebSocket |
|
||||||
| Swagger | 3.x | Documentacion API |
|
| Stripe SDK | Latest | Billing |
|
||||||
| Jest | 29.x | Testing |
|
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
| Tecnologia | Version | Proposito |
|
| Tecnologia | Version | Proposito |
|
||||||
|------------|---------|-----------|
|
|------------|---------|-----------|
|
||||||
| React | 18.x | Framework UI |
|
| React | 18.x | Framework UI |
|
||||||
| Vite | 5.x | Build tool |
|
| Vite | 5.x | Build tool |
|
||||||
| TypeScript | 5.3+ | Lenguaje |
|
|
||||||
| Zustand | 4.x | State management |
|
| Zustand | 4.x | State management |
|
||||||
| Tailwind CSS | 4.x | Styling |
|
| Tailwind CSS | 4.x | Styling |
|
||||||
| React Query | 5.x | Data fetching |
|
| React Query | 5.x | Data fetching |
|
||||||
| React Hook Form | 7.x | Formularios |
|
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
| Tecnologia | Version | Proposito |
|
| Tecnologia | Version | Proposito |
|
||||||
|------------|---------|-----------|
|
|------------|---------|-----------|
|
||||||
| PostgreSQL | 15+ | Motor BD |
|
| PostgreSQL | 16+ | Base de datos |
|
||||||
| RLS | - | Row-Level Security |
|
| RLS | - | Multi-tenancy |
|
||||||
| uuid-ossp | - | Generacion UUIDs |
|
| Redis | 7.x | Cache y colas |
|
||||||
| pg_trgm | - | Busqueda fuzzy |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Principios de Diseno
|
## Principios de Diseno
|
||||||
|
|
||||||
### 1. Multi-Tenancy First
|
### 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
|
### 2. Documentation Driven
|
||||||
Documentar antes de desarrollar. La documentacion es el contrato.
|
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
|
### 5. Single Source of Truth
|
||||||
Un lugar para cada dato. Sincronizacion automatica.
|
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
|
## Entregables por Fase
|
||||||
|
|
||||||
### Fase 1: Foundation (Actual)
|
### Fase 1: Foundation (En progreso)
|
||||||
- [ ] MGN-001 Auth completo
|
- [ ] MGN-001 Auth completo
|
||||||
- [ ] MGN-002 Users completo
|
- [ ] MGN-002 Users completo
|
||||||
- [ ] MGN-003 Roles completo
|
- [ ] MGN-003 Roles completo
|
||||||
@ -185,26 +411,46 @@ Un lugar para cada dato. Sincronizacion automatica.
|
|||||||
### Fase 3: Extended
|
### Fase 3: Extended
|
||||||
- [ ] MGN-006 Settings
|
- [ ] MGN-006 Settings
|
||||||
- [ ] MGN-007 Audit
|
- [ ] MGN-007 Audit
|
||||||
- [ ] MGN-008 Notifications
|
- [ ] MGN-008 Notifications (multicanal)
|
||||||
- [ ] MGN-009 Reports
|
- [ ] MGN-009 Reports
|
||||||
- [ ] MGN-014 CRM
|
- [ ] MGN-014 CRM
|
||||||
- [ ] MGN-015 Projects
|
- [ ] 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
|
## Referencias
|
||||||
|
|
||||||
| Recurso | Path |
|
| 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/` |
|
| Directivas | `orchestration/directivas/` |
|
||||||
| Patrones Odoo | `orchestration/directivas/DIRECTIVA-PATRONES-ODOO.md` |
|
|
||||||
| Templates | `orchestration/templates/` |
|
| Templates | `orchestration/templates/` |
|
||||||
| Catálogo central | `shared/catalog/` *(patrones reutilizables)* |
|
| Template SaaS | `projects/template-saas/` |
|
||||||
|
| MiChangarrito | `projects/michangarrito/` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Metricas de Exito
|
## Metricas de Exito
|
||||||
|
|
||||||
|
### Metricas Core
|
||||||
|
|
||||||
| Metrica | Objetivo |
|
| Metrica | Objetivo |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| Cobertura de tests | >80% |
|
| Cobertura de tests | >80% |
|
||||||
@ -212,6 +458,24 @@ Un lugar para cada dato. Sincronizacion automatica.
|
|||||||
| Reutilizacion en verticales | >60% |
|
| Reutilizacion en verticales | >60% |
|
||||||
| Tiempo de setup nueva vertical | <1 semana |
|
| 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*
|
||||||
|
|||||||
92
docs/00-vision-general/_MAP.md
Normal file
92
docs/00-vision-general/_MAP.md
Normal file
@ -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*
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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<void> | null = null;
|
|
||||||
|
|
||||||
async checkAndRefresh(): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
// 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<void> {
|
|
||||||
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 |
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
<h2>Hola {{firstName}},</h2>
|
|
||||||
|
|
||||||
<p>Recibimos una solicitud para restablecer tu contraseña.</p>
|
|
||||||
|
|
||||||
<p>Haz clic en el siguiente enlace para crear una nueva contraseña:</p>
|
|
||||||
|
|
||||||
<a href="{{resetUrl}}" style="...">Restablecer Contraseña</a>
|
|
||||||
|
|
||||||
<p><strong>Este enlace expira en 1 hora.</strong></p>
|
|
||||||
|
|
||||||
<p>Si no solicitaste este cambio, ignora este email. Tu contraseña
|
|
||||||
permanecera sin cambios.</p>
|
|
||||||
|
|
||||||
<p>Por seguridad, nunca compartas este enlace con nadie.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<small>IP: {{ipAddress}} | Fecha: {{timestamp}}</small>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -4,14 +4,15 @@
|
|||||||
|
|
||||||
| Metrica | Valor |
|
| Metrica | Valor |
|
||||||
|---------|-------|
|
|---------|-------|
|
||||||
| Total Modulos | 19 |
|
| Total Modulos | 22 |
|
||||||
| Modulos P0 (Criticos) | 4 |
|
| Modulos P0 (Criticos) | 4 |
|
||||||
| Modulos P1 (Core) | 6 |
|
| Modulos P1 (Core) | 6 |
|
||||||
| Modulos P2 (Extended) | 5 |
|
| Modulos P2 (Extended) | 5 |
|
||||||
| Modulos P3 (SaaS) | 4 |
|
| Modulos P3 (SaaS Platform) | 4 |
|
||||||
|
| Modulos P3 (IA Intelligence) | 3 |
|
||||||
| Completados | 0 |
|
| Completados | 0 |
|
||||||
| En Desarrollo | 2 |
|
| En Desarrollo | 2 |
|
||||||
| Planificados | 17 |
|
| Planificados | 20 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -52,9 +53,17 @@
|
|||||||
| Codigo | Modulo | Estado | Progreso | Docs |
|
| Codigo | Modulo | Estado | Progreso | Docs |
|
||||||
|--------|--------|--------|----------|------|
|
|--------|--------|--------|----------|------|
|
||||||
| MGN-016 | [billing](#mgn-016-billing) | Planificado | 0% | [Ver](./MGN-016-billing/) |
|
| 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-017 | [plans](#mgn-017-plans) | Planificado | 0% | [Ver](./MGN-017-plans/) |
|
||||||
| MGN-018 | [whatsapp-business](#mgn-018-whatsapp-business) | Planificado | 0% | [Ver](./MGN-018-whatsapp-business/) |
|
| MGN-018 | [webhooks](#mgn-018-webhooks) | Planificado | 0% | [Ver](./MGN-018-webhooks/) |
|
||||||
| MGN-019 | [ai-agents](#mgn-019-ai-agents) | Planificado | 0% | [Ver](./MGN-019-ai-agents/) |
|
| 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
|
### MGN-016: Billing
|
||||||
|
|
||||||
**Proposito:** Facturacion SaaS por asientos (per-seat billing)
|
**Proposito:** Suscripciones y pagos SaaS con Stripe
|
||||||
|
|
||||||
| Aspecto | Detalle |
|
| Aspecto | Detalle |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| Schema BD | `billing` |
|
| 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+ |
|
| Endpoints | 15+ |
|
||||||
| Dependencias | MGN-001, MGN-004 Tenants |
|
| Dependencias | MGN-001 Auth, MGN-004 Tenants |
|
||||||
| Usado por | MGN-017, MGN-018, MGN-019 |
|
| Usado por | MGN-017 Plans |
|
||||||
|
| Integracion | Stripe |
|
||||||
|
|
||||||
**Funcionalidades:**
|
**Funcionalidades:**
|
||||||
- Suscripciones mensuales por tenant
|
- Suscripciones recurrentes (mensual/anual)
|
||||||
- Modelo per-seat: base + extra seats × precio
|
- Trial gratuito configurable
|
||||||
- Feature flags por plan (Starter, Growth, Enterprise)
|
- Upgrade/downgrade con prorateo
|
||||||
- Gestion de propietarios de tenant
|
- Webhooks Stripe sincronizados
|
||||||
- Cupones y descuentos
|
- Portal de cliente Stripe integrado
|
||||||
- Historial de cambios de suscripcion
|
- Facturas y recibos automaticos
|
||||||
- Facturacion automatica
|
- Multiples monedas (USD, MXN)
|
||||||
- Metodos de pago (tarjetas, SPEI)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### MGN-017: Payments POS
|
### MGN-017: Plans
|
||||||
|
|
||||||
**Proposito:** Integraciones con terminales de pago (MercadoPago, Clip)
|
**Proposito:** Planes, limites y feature gating
|
||||||
|
|
||||||
| Aspecto | Detalle |
|
| Aspecto | Detalle |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| Schema BD | `integrations` |
|
| Schema BD | `plans` |
|
||||||
| Tablas | payment_providers, payment_credentials, payment_terminals, payment_transactions, refunds, webhook_logs, reconciliation_batches |
|
| Tablas | plans, plan_features, tenant_limits |
|
||||||
| Endpoints | 20+ |
|
| Endpoints | 10+ |
|
||||||
| Dependencias | MGN-001, MGN-004, MGN-010 Financial |
|
| Dependencias | MGN-016 Billing |
|
||||||
| Usado por | Verticales POS |
|
| Usado por | Todos los modulos |
|
||||||
|
|
||||||
**Funcionalidades:**
|
**Funcionalidades:**
|
||||||
- Multi-provider: MercadoPago (OAuth) y Clip (API Keys)
|
- Planes Free, Starter, Pro, Enterprise
|
||||||
- Registro de terminales por ubicacion
|
- Feature gating por plan
|
||||||
- Procesamiento de transacciones
|
- Limites numericos (usuarios, storage, etc.)
|
||||||
- Reembolsos parciales y totales
|
- Verificacion de features en tiempo real
|
||||||
- Webhooks para notificaciones
|
- Upgrade paths configurables
|
||||||
- Conciliacion de transacciones
|
|
||||||
- Generacion de asientos contables
|
**Planes propuestos:**
|
||||||
- Manejo de comisiones por provider
|
|
||||||
|
| 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 |
|
| Aspecto | Detalle |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| Schema BD | `messaging` |
|
| Schema BD | `webhooks` |
|
||||||
| Tablas | whatsapp_accounts, whatsapp_templates, whatsapp_conversations, whatsapp_messages, chatbot_flows, chatbot_nodes, chatbot_sessions, whatsapp_campaigns |
|
| Tablas | webhook_endpoints, webhook_events, webhook_deliveries |
|
||||||
| Endpoints | 25+ |
|
| Endpoints | 8+ |
|
||||||
| Dependencias | MGN-001, MGN-004, MGN-005 Catalogs |
|
| Dependencias | MGN-001 Auth, MGN-004 Tenants |
|
||||||
| Usado por | MGN-019 AI Agents |
|
| Usado por | Integraciones externas |
|
||||||
|
| Cola | BullMQ (Redis) |
|
||||||
|
|
||||||
**Funcionalidades:**
|
**Funcionalidades:**
|
||||||
- Cuentas de WhatsApp Business por tenant
|
- Registro de endpoints por tenant
|
||||||
- Templates de mensajes (HSM) aprobados por Meta
|
- Firma HMAC-SHA256 en cada request
|
||||||
- Conversaciones bidireccionales
|
- Politica de reintentos exponencial
|
||||||
- Chatbots con flujos visuales
|
- Log de entregas y respuestas
|
||||||
- Campañas de marketing masivo
|
- Eventos: user.*, subscription.*, invoice.*
|
||||||
- Opt-in/opt-out de contactos
|
|
||||||
- Metricas de entrega y lectura
|
**Eventos disponibles:**
|
||||||
- Webhooks de WhatsApp Cloud API
|
|
||||||
|
| 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 |
|
| Aspecto | Detalle |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| Schema BD | `ai_agents` |
|
| Schema BD | `feature_flags` |
|
||||||
| Tablas | agents, knowledge_bases, agent_knowledge_bases, kb_documents, kb_chunks, tool_definitions, agent_tools, conversations, messages, tool_executions, feedback, usage_logs |
|
| Tablas | flags, tenant_flags, user_flags |
|
||||||
| Endpoints | 30+ |
|
| Endpoints | 6+ |
|
||||||
| Dependencias | MGN-001, MGN-004, MGN-018 WhatsApp |
|
| 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 |
|
| Usado por | Verticales |
|
||||||
|
| Integracion | Meta WhatsApp Cloud API |
|
||||||
|
|
||||||
**Funcionalidades:**
|
**Funcionalidades:**
|
||||||
- Agentes configurables por tenant
|
- Webhook receiver Meta Cloud API
|
||||||
- Knowledge bases con documentos
|
- Procesamiento de mensajes (texto, audio, imagen)
|
||||||
- RAG con pgvector (embeddings 1536 dims)
|
- Transcripcion de audio (Whisper)
|
||||||
- Definicion de tools/funciones
|
- OCR de imagenes (Google Vision)
|
||||||
- Conversaciones con historial
|
- IA conversacional con contexto
|
||||||
- Ejecucion de tools en tiempo real
|
- Templates HSM pre-aprobados
|
||||||
- Feedback de usuarios (thumbs up/down)
|
- Envio de mensajes outbound
|
||||||
- Metricas de uso y tokens
|
|
||||||
- Integracion con WhatsApp como canal
|
**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
|
Fase 2: Core Business
|
||||||
├── MGN-005 Catalogs
|
├── MGN-005 Catalogs
|
||||||
├── Partners (parte de Catalogs)
|
├── MGN-006 Settings
|
||||||
├── Products (parte de Catalogs)
|
├── MGN-010 Financial
|
||||||
├── MGN-011 Inventory
|
├── MGN-011 Inventory
|
||||||
├── MGN-012 Purchasing
|
├── MGN-012 Purchasing
|
||||||
├── MGN-013 Sales
|
└── MGN-013 Sales
|
||||||
└── MGN-010 Financial
|
|
||||||
|
|
||||||
Fase 3: Extended
|
Fase 3: Extended
|
||||||
├── MGN-006 Settings
|
|
||||||
├── MGN-007 Audit
|
├── MGN-007 Audit
|
||||||
├── MGN-008 Notifications
|
├── MGN-008 Notifications
|
||||||
├── MGN-009 Reports
|
├── MGN-009 Reports
|
||||||
@ -499,9 +612,14 @@ Fase 3: Extended
|
|||||||
|
|
||||||
Fase 4: SaaS Platform
|
Fase 4: SaaS Platform
|
||||||
├── MGN-016 Billing (depende de MGN-004)
|
├── MGN-016 Billing (depende de MGN-004)
|
||||||
├── MGN-017 Payments POS (depende de MGN-010)
|
├── MGN-017 Plans (depende de MGN-016)
|
||||||
├── MGN-018 WhatsApp Business (depende de MGN-005)
|
├── MGN-018 Webhooks (depende de MGN-004)
|
||||||
└── MGN-019 AI Agents (depende de MGN-018)
|
└── 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
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
MGN001[MGN-001 Auth] --> MGN002[MGN-002 Users]
|
subgraph "Fase 1: Foundation"
|
||||||
MGN001 --> MGN004[MGN-004 Tenants]
|
MGN001[MGN-001 Auth]
|
||||||
MGN002 --> MGN003[MGN-003 Roles]
|
MGN002[MGN-002 Users]
|
||||||
MGN004 --> MGN005[MGN-005 Catalogs]
|
MGN003[MGN-003 Roles]
|
||||||
MGN005 --> MGN010[MGN-010 Financial]
|
MGN004[MGN-004 Tenants]
|
||||||
MGN005 --> MGN011[MGN-011 Inventory]
|
end
|
||||||
MGN011 --> MGN012[MGN-012 Purchasing]
|
|
||||||
MGN011 --> MGN013[MGN-013 Sales]
|
|
||||||
MGN010 --> MGN012
|
|
||||||
MGN010 --> MGN013
|
|
||||||
MGN005 --> MGN014[MGN-014 CRM]
|
|
||||||
MGN002 --> MGN015[MGN-015 Projects]
|
|
||||||
|
|
||||||
%% SaaS Platform Modules
|
subgraph "Fase 2-3: Core/Extended"
|
||||||
MGN004 --> MGN016[MGN-016 Billing]
|
MGN005[MGN-005 Catalogs]
|
||||||
MGN010 --> MGN017[MGN-017 Payments POS]
|
MGN010[MGN-010 Financial]
|
||||||
MGN005 --> MGN018[MGN-018 WhatsApp]
|
MGN011[MGN-011 Inventory]
|
||||||
MGN018 --> MGN019[MGN-019 AI Agents]
|
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 --> MGN017
|
||||||
MGN016 --> MGN018
|
MGN004 --> MGN018
|
||||||
|
MGN004 --> MGN019
|
||||||
|
|
||||||
|
%% IA Intelligence
|
||||||
|
MGN004 --> MGN020
|
||||||
|
MGN020 --> MGN021
|
||||||
|
MGN020 --> MGN022
|
||||||
|
MGN021 --> MGN022
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Proximas Acciones
|
## Proximas Acciones
|
||||||
|
|
||||||
1. **Completar documentacion MGN-001 Auth**
|
1. **Completar documentacion Fase 1: Foundation**
|
||||||
- DDL Specification
|
- MGN-001 Auth: DDL, Backend Spec, User Stories
|
||||||
- Especificacion Backend
|
- MGN-002 Users: DDL, Backend Spec, User Stories
|
||||||
- User Stories
|
- MGN-003 Roles: RBAC design
|
||||||
|
- MGN-004 Tenants: RLS implementation
|
||||||
|
|
||||||
2. **Completar documentacion MGN-002 Users**
|
2. **Planificar Fase 4: SaaS Platform**
|
||||||
- Mismos entregables
|
- 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**
|
3. **Planificar Fase 5: IA Intelligence**
|
||||||
- Requerimientos funcionales
|
- MGN-020 AI Integration: OpenRouter gateway
|
||||||
- Diseno de RBAC
|
- MGN-021 WhatsApp Business: Meta Cloud API
|
||||||
|
- MGN-022 MCP Server: Business tools
|
||||||
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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*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*
|
||||||
|
|||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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<Result> {
|
|
||||||
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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 | "<html>...</html>" |
|
|
||||||
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* |
|
|
||||||
| +--------------------------------------------------------------+ |
|
|
||||||
| | <h1>Hola {{user.firstName}},</h1> | |
|
|
||||||
| | <p>Hemos recibido tu pedido {{order.number}}.</p> | |
|
|
||||||
| | <table> | |
|
|
||||||
| | <tr><td>Total:</td><td>{{order.total}}</td></tr> | |
|
|
||||||
| | </table> | |
|
|
||||||
| +--------------------------------------------------------------+ |
|
|
||||||
| |
|
|
||||||
| [Preview] |
|
|
||||||
| |
|
|
||||||
| [Cancelar] [=== Guardar ===] |
|
|
||||||
+------------------------------------------------------------------+
|
|
||||||
|
|
||||||
Preview Modal:
|
|
||||||
+------------------------------------------------------------------+
|
|
||||||
| PREVIEW [X] |
|
|
||||||
+------------------------------------------------------------------+
|
|
||||||
| De: Mi Empresa <noreply@empresa.com> |
|
|
||||||
| 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<EmailJobData>) {
|
|
||||||
// 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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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<string, { enabled: boolean; channels: string[] }>;
|
|
||||||
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 |
|
|
||||||
@ -4,7 +4,7 @@
|
|||||||
**Nombre:** Reportes y Dashboards
|
**Nombre:** Reportes y Dashboards
|
||||||
**Fase:** 02 - Core Business
|
**Fase:** 02 - Core Business
|
||||||
**Story Points:** 35 SP
|
**Story Points:** 35 SP
|
||||||
**Estado:** COMPLETADO (Sprint 8-11)
|
**Estado:** COMPLETADO (Sprint 8-13)
|
||||||
**Ultima actualizacion:** 2026-01-07
|
**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 9 | Frontend | Dashboard UI | 24 |
|
||||||
| Sprint 10 | Frontend | Report Builder UI | 13 |
|
| Sprint 10 | Frontend | Report Builder UI | 13 |
|
||||||
| Sprint 11 | Frontend | Scheduled Reports UI | 11 |
|
| 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 |
|
| Componente | Descripcion | Estado |
|
||||||
|------|-------------|-----------|
|
|------------|-------------|--------|
|
||||||
| PDF Export | Integracion con puppeteer para PDF | P2 |
|
| PdfService | Servicio Puppeteer para generacion PDF | Implementado |
|
||||||
| Tests | Tests unitarios para componentes | P2 |
|
| ReportTemplates | Templates HTML (tabular, financial, trialBalance) | Implementado |
|
||||||
| Pages | Crear paginas/rutas para features | P1 |
|
| 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
|
**Fecha:** 2026-01-07
|
||||||
**Sprint:** 11 - COMPLETADO
|
**Tarea:** BE-026 - PDF Export COMPLETADO
|
||||||
|
|||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -7,7 +7,7 @@ epic_name: Reports
|
|||||||
phase: 2
|
phase: 2
|
||||||
phase_name: Core Business
|
phase_name: Core Business
|
||||||
story_points: 35
|
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"
|
last_updated: "2026-01-07"
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@ -253,12 +253,37 @@ implementation:
|
|||||||
|
|
||||||
- name: ExportService
|
- name: ExportService
|
||||||
file: apps/backend/src/modules/reports/export.service.ts
|
file: apps/backend/src/modules/reports/export.service.ts
|
||||||
status: pending
|
status: completed
|
||||||
requirement: RF-REPORT-001
|
requirement: RF-REPORT-001
|
||||||
methods:
|
methods:
|
||||||
- {name: toPdf, description: Exportar a PDF}
|
- {name: export, description: Exportar datos a formato especificado}
|
||||||
- {name: toExcel, description: Exportar a Excel}
|
- {name: exportToPdf, description: Exportar a PDF usando PdfService}
|
||||||
- {name: toCsv, description: Exportar a CSV}
|
- {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
|
- name: DashboardsService
|
||||||
file: apps/backend/src/modules/reports/dashboards.service.ts
|
file: apps/backend/src/modules/reports/dashboards.service.ts
|
||||||
@ -310,9 +335,19 @@ implementation:
|
|||||||
description: Ejecutar reporte
|
description: Ejecutar reporte
|
||||||
requirement: RF-REPORT-001
|
requirement: RF-REPORT-001
|
||||||
|
|
||||||
|
- method: POST
|
||||||
|
path: /api/v1/reports/executions/:id/export
|
||||||
|
description: Exportar resultado de ejecucion
|
||||||
|
requirement: RF-REPORT-001
|
||||||
|
|
||||||
- method: GET
|
- method: GET
|
||||||
path: /api/v1/reports/:id/export/:format
|
path: /api/v1/reports/quick/trial-balance/export
|
||||||
description: Exportar reporte
|
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
|
requirement: RF-REPORT-001
|
||||||
|
|
||||||
- method: GET
|
- method: GET
|
||||||
@ -430,6 +465,74 @@ implementation:
|
|||||||
- {package: "react-grid-layout", version: "^1.4.4", purpose: "Grid drag & drop"}
|
- {package: "react-grid-layout", version: "^1.4.4", purpose: "Grid drag & drop"}
|
||||||
- {package: "recharts", version: "^2.10.x", purpose: "Charts"}
|
- {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
|
# DEPENDENCIAS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@ -458,7 +561,7 @@ dependencies:
|
|||||||
metrics:
|
metrics:
|
||||||
story_points:
|
story_points:
|
||||||
estimated: 35
|
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:
|
documentation:
|
||||||
requirements: 4
|
requirements: 4
|
||||||
@ -467,9 +570,10 @@ metrics:
|
|||||||
|
|
||||||
files:
|
files:
|
||||||
database: 1 # 14-reports.sql (12 tablas)
|
database: 1 # 14-reports.sql (12 tablas)
|
||||||
backend: 14
|
backend: 17 # +3: pdf.service.ts, templates/report-templates.ts, export.service.ts updated
|
||||||
frontend: 48 # Sprint 9 (24) + Sprint 10 (13) + Sprint 11 (11)
|
frontend: 56 # Sprint 9 (24) + Sprint 10 (13) + Sprint 11 (11) + Sprint 12 (4) + Sprint 13 (4)
|
||||||
total: 63
|
tests: 4 # Sprint 13
|
||||||
|
total: 78
|
||||||
|
|
||||||
sprints:
|
sprints:
|
||||||
- sprint: 8
|
- sprint: 8
|
||||||
@ -491,6 +595,16 @@ metrics:
|
|||||||
status: completed
|
status: completed
|
||||||
date: "2026-01-07"
|
date: "2026-01-07"
|
||||||
feature: scheduled-reports
|
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
|
# HISTORIAL
|
||||||
@ -572,4 +686,40 @@ history:
|
|||||||
- "4 formatos de exportacion: PDF, Excel, CSV, JSON"
|
- "4 formatos de exportacion: PDF, Excel, CSV, JSON"
|
||||||
- "TypeScript build validado"
|
- "TypeScript build validado"
|
||||||
- "Vite 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"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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<CloseResult> {
|
|
||||||
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 |
|
|
||||||
@ -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<PostResult> {
|
|
||||||
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 |
|
|
||||||
272
docs/03-fase-mobile/MOB-001-foundation/_MAP.md
Normal file
272
docs/03-fase-mobile/MOB-001-foundation/_MAP.md
Normal file
@ -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: ['<rootDir>/jest.setup.js'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@/(.*)$': '<rootDir>/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
|
||||||
47
docs/03-fase-vertical/MGN-011-sales/README.md
Normal file
47
docs/03-fase-vertical/MGN-011-sales/README.md
Normal file
@ -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*
|
||||||
@ -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<CustomerGroup>` | Obtiene grupo por ID con sus miembros |
|
||||||
|
| `create` | `dto: CreateCustomerGroupDto`, `tenantId: string`, `userId: string` | `Promise<CustomerGroup>` | Crea nuevo grupo de clientes |
|
||||||
|
| `update` | `id: string`, `dto: UpdateCustomerGroupDto`, `tenantId: string` | `Promise<CustomerGroup>` | Actualiza grupo existente |
|
||||||
|
| `delete` | `id: string`, `tenantId: string` | `Promise<void>` | Elimina grupo (solo si no tiene miembros) |
|
||||||
|
| `addMember` | `groupId: string`, `partnerId: string`, `tenantId: string` | `Promise<CustomerGroupMember>` | Agrega cliente al grupo |
|
||||||
|
| `removeMember` | `groupId: string`, `memberId: string`, `tenantId: string` | `Promise<void>` | 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<SalesTeam>` | Obtiene equipo por ID con miembros |
|
||||||
|
| `create` | `dto: CreateSalesTeamDto`, `tenantId: string`, `userId: string` | `Promise<SalesTeam>` | Crea nuevo equipo de ventas |
|
||||||
|
| `update` | `id: string`, `dto: UpdateSalesTeamDto`, `tenantId: string`, `userId: string` | `Promise<SalesTeam>` | Actualiza equipo existente |
|
||||||
|
| `addMember` | `teamId: string`, `userId: string`, `role: string`, `tenantId: string` | `Promise<SalesTeamMember>` | Agrega usuario al equipo |
|
||||||
|
| `removeMember` | `teamId: string`, `memberId: string`, `tenantId: string` | `Promise<void>` | 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<Pricelist>` | Obtiene lista de precios con items |
|
||||||
|
| `create` | `dto: CreatePricelistDto`, `tenantId: string`, `userId: string` | `Promise<Pricelist>` | Crea nueva lista de precios |
|
||||||
|
| `update` | `id: string`, `dto: UpdatePricelistDto`, `tenantId: string`, `userId: string` | `Promise<Pricelist>` | Actualiza lista de precios |
|
||||||
|
| `addItem` | `pricelistId: string`, `dto: CreatePricelistItemDto`, `tenantId: string`, `userId: string` | `Promise<PricelistItem>` | Agrega item a lista de precios |
|
||||||
|
| `removeItem` | `pricelistId: string`, `itemId: string`, `tenantId: string` | `Promise<void>` | Elimina item de lista de precios |
|
||||||
|
| `getProductPrice` | `productId: string`, `pricelistId: string`, `quantity: number` | `Promise<number \| null>` | 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<SalesOrder>` | Obtiene orden por ID con lineas |
|
||||||
|
| `create` | `dto: CreateSalesOrderDto`, `tenantId: string`, `userId: string` | `Promise<SalesOrder>` | Crea nueva orden de venta |
|
||||||
|
| `update` | `id: string`, `dto: UpdateSalesOrderDto`, `tenantId: string`, `userId: string` | `Promise<SalesOrder>` | Actualiza orden (solo en estado draft) |
|
||||||
|
| `delete` | `id: string`, `tenantId: string` | `Promise<void>` | Elimina orden (solo en estado draft) |
|
||||||
|
| `addLine` | `orderId: string`, `dto: CreateSalesOrderLineDto`, `tenantId: string`, `userId: string` | `Promise<SalesOrderLine>` | Agrega linea a orden |
|
||||||
|
| `updateLine` | `orderId: string`, `lineId: string`, `dto: UpdateSalesOrderLineDto`, `tenantId: string` | `Promise<SalesOrderLine>` | Actualiza linea de orden |
|
||||||
|
| `removeLine` | `orderId: string`, `lineId: string`, `tenantId: string` | `Promise<void>` | Elimina linea de orden |
|
||||||
|
| `confirm` | `id: string`, `tenantId: string`, `userId: string` | `Promise<SalesOrder>` | Confirma orden (draft -> sent) |
|
||||||
|
| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise<SalesOrder>` | 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<Quotation>` | Obtiene cotizacion por ID con lineas |
|
||||||
|
| `create` | `dto: CreateQuotationDto`, `tenantId: string`, `userId: string` | `Promise<Quotation>` | Crea nueva cotizacion |
|
||||||
|
| `update` | `id: string`, `dto: UpdateQuotationDto`, `tenantId: string`, `userId: string` | `Promise<Quotation>` | Actualiza cotizacion (solo en draft) |
|
||||||
|
| `delete` | `id: string`, `tenantId: string` | `Promise<void>` | Elimina cotizacion (solo en draft) |
|
||||||
|
| `addLine` | `quotationId: string`, `dto: CreateQuotationLineDto`, `tenantId: string`, `userId: string` | `Promise<QuotationLine>` | Agrega linea a cotizacion |
|
||||||
|
| `updateLine` | `quotationId: string`, `lineId: string`, `dto: UpdateQuotationLineDto`, `tenantId: string` | `Promise<QuotationLine>` | Actualiza linea de cotizacion |
|
||||||
|
| `removeLine` | `quotationId: string`, `lineId: string`, `tenantId: string` | `Promise<void>` | Elimina linea de cotizacion |
|
||||||
|
| `send` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Quotation>` | 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<Quotation>` | 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
|
||||||
44
docs/03-fase-vertical/MGN-012-purchases/README.md
Normal file
44
docs/03-fase-vertical/MGN-012-purchases/README.md
Normal file
@ -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*
|
||||||
@ -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<PurchaseOrder>` | Obtiene una orden por ID con sus lineas |
|
||||||
|
| `create` | `dto: CreatePurchaseOrderDto`, `tenantId: string`, `userId: string` | `Promise<PurchaseOrder>` | Crea nueva orden de compra con lineas (transaccion) |
|
||||||
|
| `update` | `id: string`, `dto: UpdatePurchaseOrderDto`, `tenantId: string`, `userId: string` | `Promise<PurchaseOrder>` | Actualiza orden en estado draft |
|
||||||
|
| `confirm` | `id: string`, `tenantId: string`, `userId: string` | `Promise<PurchaseOrder>` | Confirma orden (draft -> confirmed) |
|
||||||
|
| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise<PurchaseOrder>` | Cancela orden (no aplica a done) |
|
||||||
|
| `delete` | `id: string`, `tenantId: string` | `Promise<void>` | 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<Rfq>` | Obtiene RFQ por ID con lineas y partners |
|
||||||
|
| `create` | `dto: CreateRfqDto`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Crea nueva RFQ con lineas (transaccion) |
|
||||||
|
| `update` | `id: string`, `dto: UpdateRfqDto`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Actualiza RFQ en estado draft |
|
||||||
|
| `addLine` | `rfqId: string`, `dto: CreateRfqLineDto`, `tenantId: string` | `Promise<RfqLine>` | Agrega linea a RFQ en draft |
|
||||||
|
| `updateLine` | `rfqId: string`, `lineId: string`, `dto: UpdateRfqLineDto`, `tenantId: string` | `Promise<RfqLine>` | Actualiza linea de RFQ en draft |
|
||||||
|
| `removeLine` | `rfqId: string`, `lineId: string`, `tenantId: string` | `Promise<void>` | Elimina linea (minimo 1 linea requerida) |
|
||||||
|
| `send` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Envia RFQ (draft -> sent) |
|
||||||
|
| `markResponded` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Marca como respondida (sent -> responded) |
|
||||||
|
| `accept` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Acepta RFQ (sent/responded -> accepted) |
|
||||||
|
| `reject` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Rechaza RFQ (sent/responded -> rejected) |
|
||||||
|
| `cancel` | `id: string`, `tenantId: string`, `userId: string` | `Promise<Rfq>` | Cancela RFQ (no aplica a accepted) |
|
||||||
|
| `delete` | `id: string`, `tenantId: string` | `Promise<void>` | 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 |
|
||||||
67
docs/03-fase-vertical/MGN-013-inventory/README.md
Normal file
67
docs/03-fase-vertical/MGN-013-inventory/README.md
Normal file
@ -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*
|
||||||
50
docs/03-fase-vertical/MGN-014-hr/README.md
Normal file
50
docs/03-fase-vertical/MGN-014-hr/README.md
Normal file
@ -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*
|
||||||
1391
docs/03-fase-vertical/MGN-014-hr/especificaciones/ET-HR-BACKEND.md
Normal file
1391
docs/03-fase-vertical/MGN-014-hr/especificaciones/ET-HR-BACKEND.md
Normal file
File diff suppressed because it is too large
Load Diff
48
docs/03-fase-vertical/MGN-015-crm/README.md
Normal file
48
docs/03-fase-vertical/MGN-015-crm/README.md
Normal file
@ -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*
|
||||||
54
docs/03-fase-vertical/MGN-016-projects/README.md
Normal file
54
docs/03-fase-vertical/MGN-016-projects/README.md
Normal file
@ -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*
|
||||||
57
docs/03-fase-vertical/MGN-017-partners/README.md
Normal file
57
docs/03-fase-vertical/MGN-017-partners/README.md
Normal file
@ -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*
|
||||||
43
docs/03-fase-vertical/MGN-018-companies/README.md
Normal file
43
docs/03-fase-vertical/MGN-018-companies/README.md
Normal file
@ -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*
|
||||||
55
docs/03-fase-vertical/README.md
Normal file
55
docs/03-fase-vertical/README.md
Normal file
@ -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*
|
||||||
@ -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 | - | - | [ ] |
|
|
||||||
@ -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 | - | - | [ ] |
|
|
||||||
@ -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 | - | - | [ ] |
|
|
||||||
@ -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<TokenPair> {
|
|
||||||
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 | - | - | [ ] |
|
|
||||||
@ -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<void> {
|
|
||||||
const key = `blacklist:${jti}`;
|
|
||||||
await this.redis.set(key, '1', 'EX', expiresIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
async isBlacklisted(jti: string): Promise<boolean> {
|
|
||||||
const key = `blacklist:${jti}`;
|
|
||||||
const result = await this.redis.get(key);
|
|
||||||
return result !== null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logout All (Logout Global)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async logoutAll(userId: string): Promise<void> {
|
|
||||||
// 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 | - | - | [ ] |
|
|
||||||
@ -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<void> {
|
|
||||||
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
|
|
||||||
<h2>Recuperacion de Contraseña</h2>
|
|
||||||
<p>Hola {{userName}},</p>
|
|
||||||
<p>Recibimos una solicitud para restablecer tu contraseña.</p>
|
|
||||||
<p>Haz clic en el siguiente enlace para crear una nueva contraseña:</p>
|
|
||||||
<a href="{{resetUrl}}">Restablecer Contraseña</a>
|
|
||||||
<p>Este enlace expira en 1 hora.</p>
|
|
||||||
<p>Si no solicitaste este cambio, ignora este email.</p>
|
|
||||||
<p><strong>Por seguridad, nunca compartas este enlace.</strong></p>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 | - | - | [ ] |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
@ -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 |
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user