[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

- 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:
rckrdmrd 2026-01-10 08:53:05 -06:00
parent f95b8d4577
commit 0086695b4c
240 changed files with 44314 additions and 25493 deletions

381
.github/workflows/ci.yml vendored Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -25,8 +25,10 @@
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"node-cron": "^4.2.1",
"nodemailer": "^7.0.12",
"otplib": "^12.0.1",
"pg": "^8.11.3",
"puppeteer": "^22.15.0",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"socket.io": "^4.7.4",
@ -48,6 +50,7 @@
"@types/morgan": "^1.9.9",
"@types/node": "^20.10.4",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^7.0.4",
"@types/pg": "^8.10.9",
"@types/socket.io": "^3.0.0",
"@types/supertest": "^6.0.2",

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import { z } from 'zod';
import { leadsService, CreateLeadDto, UpdateLeadDto, LeadFilters } from './leads.service.js';
import { opportunitiesService, CreateOpportunityDto, UpdateOpportunityDto, OpportunityFilters } from './opportunities.service.js';
import { stagesService, CreateLeadStageDto, UpdateLeadStageDto, CreateOpportunityStageDto, UpdateOpportunityStageDto, CreateLostReasonDto, UpdateLostReasonDto } from './stages.service.js';
import { tagsService, CreateTagDto, UpdateTagDto, TagFilters } from './tags.service.js';
import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js';
import { ValidationError } from '../../shared/errors/index.js';
@ -677,6 +678,94 @@ class CrmController {
next(error);
}
}
// ========== TAGS ==========
async getTags(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<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();

View File

@ -123,4 +123,22 @@ router.delete('/lost-reasons/:id', requireRoles('admin', 'super_admin'), (req, r
crmController.deleteLostReason(req, res, next)
);
// ========== TAGS ==========
router.get('/tags', (req, res, next) => crmController.getTags(req, res, next));
router.get('/tags/:id', (req, res, next) => crmController.getTag(req, res, next));
router.post('/tags', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
crmController.createTag(req, res, next)
);
router.put('/tags/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
crmController.updateTag(req, res, next)
);
router.delete('/tags/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
crmController.deleteTag(req, res, next)
);
export default router;

View File

@ -21,3 +21,15 @@ export { Tax, TaxType } from './tax.entity.js';
// Fiscal period entities
export { FiscalYear, FiscalPeriodStatus } from './fiscal-year.entity.js';
export { FiscalPeriod } from './fiscal-period.entity.js';
// Catalog entities (read-only)
export { Incoterm } from './incoterm.entity.js';
export { PaymentMethodCatalog } from './payment-method.entity.js';
// Payment term entities
export { PaymentTerm } from './payment-term.entity.js';
export { PaymentTermLine, PaymentTermValue } from './payment-term-line.entity.js';
// Reconcile model entities
export { ReconcileModel, ReconcileModelType } from './reconcile-model.entity.js';
export { ReconcileModelLine } from './reconcile-model-line.entity.js';

View File

@ -6,6 +6,10 @@ import { journalEntriesService, CreateJournalEntryDto, UpdateJournalEntryDto, Jo
import { invoicesService, CreateInvoiceDto, UpdateInvoiceDto, CreateInvoiceLineDto, UpdateInvoiceLineDto, InvoiceFilters } from './invoices.service.js';
import { paymentsService, CreatePaymentDto, UpdatePaymentDto, ReconcileDto, PaymentFilters } from './payments.service.js';
import { taxesService, CreateTaxDto, UpdateTaxDto, TaxFilters } from './taxes.service.js';
import { incotermsService, IncotermFilters } from './incoterms.service.js';
import { paymentMethodsService, PaymentMethodFilters } from './payment-methods.service.js';
import { paymentTermsService, CreatePaymentTermDto, UpdatePaymentTermDto, PaymentTermLineDto } from './payment-terms.service.js';
import { reconcileModelsService, CreateReconcileModelDto, UpdateReconcileModelDto, ReconcileModelLineDto, ReconcileModelFilters } from './reconcile-models.service.js';
import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js';
import { ValidationError } from '../../shared/errors/index.js';
@ -262,6 +266,134 @@ const taxQuerySchema = z.object({
limit: z.coerce.number().int().positive().max(100).default(20),
});
// ========== INCOTERM SCHEMAS (read-only) ==========
const incotermQuerySchema = z.object({
is_active: z.coerce.boolean().optional(),
search: z.string().optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(50),
});
// ========== PAYMENT METHOD SCHEMAS (read-only) ==========
const paymentMethodQuerySchema = z.object({
payment_type: z.enum(['inbound', 'outbound']).optional(),
is_active: z.coerce.boolean().optional(),
search: z.string().optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(50),
});
// ========== PAYMENT TERMS SCHEMAS ==========
const createPaymentTermSchema = z.object({
company_id: z.string().uuid(),
name: z.string().min(1).max(100),
code: z.string().min(1).max(20),
terms: z.array(z.object({
days: z.number().int().min(0),
percent: z.number().min(0).max(100),
})).optional(),
active: z.boolean().default(true),
lines: z.array(z.object({
value: z.enum(['balance', 'percent', 'fixed']).optional(),
value_amount: z.number().optional(),
nb_days: z.number().int().min(0).optional(),
delay_type: z.string().optional(),
day_of_the_month: z.number().int().min(1).max(31).nullable().optional(),
sequence: z.number().int().optional(),
})).optional(),
});
const updatePaymentTermSchema = z.object({
name: z.string().min(1).max(100).optional(),
code: z.string().min(1).max(20).optional(),
terms: z.array(z.object({
days: z.number().int().min(0),
percent: z.number().min(0).max(100),
})).optional(),
active: z.boolean().optional(),
});
const paymentTermQuerySchema = z.object({
company_id: z.string().uuid().optional(),
active: z.coerce.boolean().optional(),
search: z.string().optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(50),
});
const paymentTermLineSchema = z.object({
value: z.enum(['balance', 'percent', 'fixed']).optional(),
value_amount: z.number().optional(),
nb_days: z.number().int().min(0).optional(),
delay_type: z.string().optional(),
day_of_the_month: z.number().int().min(1).max(31).nullable().optional(),
sequence: z.number().int().optional(),
});
// ========== RECONCILE MODEL SCHEMAS ==========
const createReconcileModelSchema = z.object({
name: z.string().min(1).max(255),
company_id: z.string().uuid().optional().nullable(),
sequence: z.number().int().optional(),
rule_type: z.enum(['writeoff_button', 'writeoff_suggestion', 'invoice_matching']).optional(),
auto_reconcile: z.boolean().optional(),
match_nature: z.enum(['amount_received', 'amount_paid', 'both']).optional(),
match_amount: z.enum(['lower', 'greater', 'between', 'any']).optional(),
match_amount_min: z.number().optional().nullable(),
match_amount_max: z.number().optional().nullable(),
match_label: z.string().optional().nullable(),
match_label_param: z.string().optional().nullable(),
match_partner: z.boolean().optional(),
match_partner_ids: z.array(z.string().uuid()).optional().nullable(),
is_active: z.boolean().default(true),
lines: z.array(z.object({
account_id: z.string().uuid(),
journal_id: z.string().uuid().optional().nullable(),
label: z.string().optional().nullable(),
amount_type: z.enum(['percentage', 'fixed', 'regex']).optional(),
amount_value: z.number().optional(),
tax_ids: z.array(z.string().uuid()).optional().nullable(),
analytic_account_id: z.string().uuid().optional().nullable(),
sequence: z.number().int().optional(),
})).optional(),
});
const updateReconcileModelSchema = z.object({
name: z.string().min(1).max(255).optional(),
sequence: z.number().int().optional(),
rule_type: z.enum(['writeoff_button', 'writeoff_suggestion', 'invoice_matching']).optional(),
auto_reconcile: z.boolean().optional(),
match_nature: z.enum(['amount_received', 'amount_paid', 'both']).optional(),
match_amount: z.enum(['lower', 'greater', 'between', 'any']).optional(),
match_amount_min: z.number().optional().nullable(),
match_amount_max: z.number().optional().nullable(),
match_label: z.string().optional().nullable(),
match_label_param: z.string().optional().nullable(),
match_partner: z.boolean().optional(),
match_partner_ids: z.array(z.string().uuid()).optional().nullable(),
is_active: z.boolean().optional(),
});
const reconcileModelQuerySchema = z.object({
company_id: z.string().uuid().optional(),
rule_type: z.enum(['writeoff_button', 'writeoff_suggestion', 'invoice_matching']).optional(),
is_active: z.coerce.boolean().optional(),
search: z.string().optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(50),
});
const reconcileModelLineSchema = z.object({
account_id: z.string().uuid(),
journal_id: z.string().uuid().optional().nullable(),
label: z.string().optional().nullable(),
amount_type: z.enum(['percentage', 'fixed', 'regex']).optional(),
amount_value: z.number().optional(),
tax_ids: z.array(z.string().uuid()).optional().nullable(),
analytic_account_id: z.string().uuid().optional().nullable(),
sequence: z.number().int().optional(),
});
class FinancialController {
// ========== ACCOUNT TYPES ==========
async getAccountTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
@ -777,6 +909,430 @@ class FinancialController {
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();

View File

@ -147,4 +147,74 @@ router.delete('/taxes/:id', requireRoles('admin', 'super_admin'), (req, res, nex
financialController.deleteTax(req, res, next)
);
// ========== INCOTERMS (read-only) ==========
router.get('/incoterms', (req, res, next) => financialController.getIncoterms(req, res, next));
router.get('/incoterms/:id', (req, res, next) => financialController.getIncoterm(req, res, next));
// ========== PAYMENT METHODS (read-only) ==========
router.get('/payment-methods', (req, res, next) => financialController.getPaymentMethods(req, res, next));
router.get('/payment-methods/:id', (req, res, next) => financialController.getPaymentMethod(req, res, next));
// ========== PAYMENT TERMS ==========
router.get('/payment-terms', requireRoles('admin', 'accountant', 'manager', 'sales', 'super_admin'), (req, res, next) =>
financialController.getPaymentTerms(req, res, next)
);
router.get('/payment-terms/:id', requireRoles('admin', 'accountant', 'manager', 'sales', 'super_admin'), (req, res, next) =>
financialController.getPaymentTerm(req, res, next)
);
router.post('/payment-terms', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.createPaymentTerm(req, res, next)
);
router.put('/payment-terms/:id', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.updatePaymentTerm(req, res, next)
);
router.delete('/payment-terms/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
financialController.deletePaymentTerm(req, res, next)
);
// Payment Term Lines
router.get('/payment-terms/:id/lines', requireRoles('admin', 'accountant', 'manager', 'sales', 'super_admin'), (req, res, next) =>
financialController.getPaymentTermLines(req, res, next)
);
router.post('/payment-terms/:id/lines', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.addPaymentTermLine(req, res, next)
);
router.put('/payment-terms/:id/lines/:lineId', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.updatePaymentTermLine(req, res, next)
);
router.delete('/payment-terms/:id/lines/:lineId', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.removePaymentTermLine(req, res, next)
);
// ========== RECONCILE MODELS ==========
router.get('/reconcile-models', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.getReconcileModels(req, res, next)
);
router.get('/reconcile-models/:id', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.getReconcileModel(req, res, next)
);
router.post('/reconcile-models', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.createReconcileModel(req, res, next)
);
router.put('/reconcile-models/:id', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.updateReconcileModel(req, res, next)
);
router.delete('/reconcile-models/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
financialController.deleteReconcileModel(req, res, next)
);
// Reconcile Model Lines
router.get('/reconcile-models/:id/lines', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.getReconcileModelLines(req, res, next)
);
router.post('/reconcile-models/:id/lines', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.addReconcileModelLine(req, res, next)
);
router.put('/reconcile-models/:id/lines/:lineId', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.updateReconcileModelLine(req, res, next)
);
router.delete('/reconcile-models/:id/lines/:lineId', requireRoles('admin', 'accountant', 'super_admin'), (req, res, next) =>
financialController.removeReconcileModelLine(req, res, next)
);
export default router;

View File

@ -1,5 +1,6 @@
import { Router } from 'express';
import { hrController } from './hr.controller.js';
import { hrExtendedController } from './hr-extended.controller.js';
import { authenticate, requireRoles } from '../../shared/middleware/auth.middleware.js';
const router = Router();
@ -149,4 +150,187 @@ router.delete('/leaves/:id', requireRoles('admin', 'super_admin'), (req, res, ne
hrController.deleteLeave(req, res, next)
);
// ========== SKILL TYPES ==========
router.get('/skill-types', (req, res, next) => hrExtendedController.getSkillTypes(req, res, next));
router.get('/skill-types/:id', (req, res, next) => hrExtendedController.getSkillType(req, res, next));
router.post('/skill-types', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.createSkillType(req, res, next)
);
router.put('/skill-types/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.updateSkillType(req, res, next)
);
router.delete('/skill-types/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.deleteSkillType(req, res, next)
);
// ========== SKILLS ==========
router.get('/skills', (req, res, next) => hrExtendedController.getSkills(req, res, next));
router.get('/skills/:id', (req, res, next) => hrExtendedController.getSkill(req, res, next));
router.post('/skills', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.createSkill(req, res, next)
);
router.put('/skills/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.updateSkill(req, res, next)
);
router.delete('/skills/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.deleteSkill(req, res, next)
);
// ========== SKILL LEVELS ==========
router.get('/skill-levels', (req, res, next) => hrExtendedController.getSkillLevels(req, res, next));
router.post('/skill-levels', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.createSkillLevel(req, res, next)
);
router.put('/skill-levels/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.updateSkillLevel(req, res, next)
);
router.delete('/skill-levels/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.deleteSkillLevel(req, res, next)
);
// ========== EMPLOYEE SKILLS ==========
router.get('/employee-skills', (req, res, next) => hrExtendedController.getEmployeeSkills(req, res, next));
router.get('/employees/:employeeId/skills', (req, res, next) =>
hrExtendedController.getEmployeeSkillsByEmployee(req, res, next)
);
router.post('/employee-skills', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.createEmployeeSkill(req, res, next)
);
router.put('/employee-skills/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.updateEmployeeSkill(req, res, next)
);
router.delete('/employee-skills/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.deleteEmployeeSkill(req, res, next)
);
// ========== EXPENSE SHEETS ==========
router.get('/expense-sheets', (req, res, next) => hrExtendedController.getExpenseSheets(req, res, next));
router.get('/expense-sheets/:id', (req, res, next) => hrExtendedController.getExpenseSheet(req, res, next));
router.post('/expense-sheets', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.createExpenseSheet(req, res, next)
);
router.put('/expense-sheets/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.updateExpenseSheet(req, res, next)
);
router.post('/expense-sheets/:id/submit', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.submitExpenseSheet(req, res, next)
);
router.post('/expense-sheets/:id/approve', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.approveExpenseSheet(req, res, next)
);
router.post('/expense-sheets/:id/reject', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.rejectExpenseSheet(req, res, next)
);
router.delete('/expense-sheets/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.deleteExpenseSheet(req, res, next)
);
// ========== EXPENSES ==========
router.get('/expenses', (req, res, next) => hrExtendedController.getExpenses(req, res, next));
router.get('/expenses/:id', (req, res, next) => hrExtendedController.getExpense(req, res, next));
router.post('/expenses', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.createExpense(req, res, next)
);
router.put('/expenses/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.updateExpense(req, res, next)
);
router.delete('/expenses/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.deleteExpense(req, res, next)
);
// ========== PAYSLIP STRUCTURES ==========
router.get('/payslip-structures', (req, res, next) => hrExtendedController.getPayslipStructures(req, res, next));
router.get('/payslip-structures/:id', (req, res, next) => hrExtendedController.getPayslipStructure(req, res, next));
router.post('/payslip-structures', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.createPayslipStructure(req, res, next)
);
router.put('/payslip-structures/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.updatePayslipStructure(req, res, next)
);
router.delete('/payslip-structures/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.deletePayslipStructure(req, res, next)
);
// ========== PAYSLIPS ==========
router.get('/payslips', (req, res, next) => hrExtendedController.getPayslips(req, res, next));
router.get('/payslips/:id', (req, res, next) => hrExtendedController.getPayslip(req, res, next));
router.get('/payslips/:id/lines', (req, res, next) => hrExtendedController.getPayslipLines(req, res, next));
router.post('/payslips', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.createPayslip(req, res, next)
);
router.put('/payslips/:id', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.updatePayslip(req, res, next)
);
router.post('/payslips/:id/verify', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.verifyPayslip(req, res, next)
);
router.post('/payslips/:id/confirm', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.confirmPayslip(req, res, next)
);
router.post('/payslips/:id/cancel', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.cancelPayslip(req, res, next)
);
router.delete('/payslips/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
hrExtendedController.deletePayslip(req, res, next)
);
// Payslip Lines
router.post('/payslips/:id/lines', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.addPayslipLine(req, res, next)
);
router.put('/payslips/:id/lines/:lineId', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.updatePayslipLine(req, res, next)
);
router.delete('/payslips/:id/lines/:lineId', requireRoles('admin', 'manager', 'super_admin'), (req, res, next) =>
hrExtendedController.removePayslipLine(req, res, next)
);
export default router;

View File

@ -2,5 +2,9 @@ export * from './employees.service.js';
export * from './departments.service.js';
export * from './contracts.service.js';
export * from './leaves.service.js';
export * from './skills.service.js';
export * from './expenses.service.js';
export * from './payslips.service.js';
export * from './hr.controller.js';
export * from './hr-extended.controller.js';
export { default as hrRoutes } from './hr.routes.js';

View File

@ -9,3 +9,4 @@ export * from './stock-move.entity.js';
export * from './inventory-adjustment.entity.js';
export * from './inventory-adjustment-line.entity.js';
export * from './stock-valuation-layer.entity.js';
export * from './package-type.entity.js';

View File

@ -6,6 +6,7 @@ import { locationsService, CreateLocationDto, UpdateLocationDto, LocationFilters
import { pickingsService, CreatePickingDto, PickingFilters } from './pickings.service.js';
import { lotsService, CreateLotDto, UpdateLotDto, LotFilters } from './lots.service.js';
import { adjustmentsService, CreateAdjustmentDto, UpdateAdjustmentDto, AdjustmentLineDto, UpdateAdjustmentLineDto, AdjustmentFilters } from './adjustments.service.js';
import { packageTypesService, CreatePackageTypeDto, UpdatePackageTypeDto, PackageTypeFilters } from './package-types.service.js';
import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js';
import { ValidationError } from '../../shared/errors/index.js';
@ -931,6 +932,123 @@ class InventoryController {
next(error);
}
}
// ========== PACKAGE TYPES ==========
async getPackageTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<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();

View File

@ -171,4 +171,21 @@ router.post('/valuation/consume', requireRoles('admin', 'manager', 'super_admin'
valuationController.consumeFifo(req, res, next)
);
// ========== PACKAGE TYPES ==========
router.get('/package-types', (req, res, next) => inventoryController.getPackageTypes(req, res, next));
router.get('/package-types/:id', (req, res, next) => inventoryController.getPackageType(req, res, next));
router.post('/package-types', requireRoles('admin', 'manager', 'warehouse', 'super_admin'), (req, res, next) =>
inventoryController.createPackageType(req, res, next)
);
router.put('/package-types/:id', requireRoles('admin', 'manager', 'warehouse', 'super_admin'), (req, res, next) =>
inventoryController.updatePackageType(req, res, next)
);
router.delete('/package-types/:id', requireRoles('admin', 'super_admin'), (req, res, next) =>
inventoryController.deletePackageType(req, res, next)
);
export default router;

View File

@ -2,6 +2,9 @@ import { Response, NextFunction } from 'express';
import { z } from 'zod';
import { AuthenticatedRequest } from '../../shared/types/index.js';
import { reportsService } from './reports.service.js';
import { exportService, ExportFormat } from './export.service.js';
import { generateTrialBalance as generateTrialBalanceTemplate } from './templates/report-templates.js';
import { pdfService } from './pdf.service.js';
// ============================================================================
// VALIDATION SCHEMAS
@ -60,6 +63,13 @@ const generalLedgerSchema = z.object({
date_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
});
const exportSchema = z.object({
format: z.enum(['pdf', 'xlsx', 'csv', 'json', 'html']).default('pdf'),
title: z.string().optional(),
orientation: z.enum(['portrait', 'landscape']).optional(),
pageSize: z.enum(['A4', 'Letter', 'Legal']).optional(),
});
// ============================================================================
// CONTROLLER
// ============================================================================
@ -429,6 +439,202 @@ class ReportsController {
next(error);
}
}
// ==================== EXPORT ====================
/**
* POST /reports/executions/:id/export
* Export an existing execution to a specific format
*/
async exportExecution(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): Promise<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();

View File

@ -16,11 +16,25 @@ router.get('/quick/trial-balance',
(req, res, next) => reportsController.getTrialBalance(req, res, next)
);
router.get('/quick/trial-balance/export',
requireRoles('admin', 'manager', 'accountant', 'super_admin'),
(req, res, next) => reportsController.exportTrialBalance(req, res, next)
);
router.get('/quick/general-ledger',
requireRoles('admin', 'manager', 'accountant', 'super_admin'),
(req, res, next) => reportsController.getGeneralLedger(req, res, next)
);
// ============================================================================
// PDF SERVICE
// ============================================================================
router.get('/pdf/health',
requireRoles('admin', 'super_admin'),
(req, res, next) => reportsController.checkPdfHealth(req, res, next)
);
// ============================================================================
// DEFINITIONS
// ============================================================================
@ -65,6 +79,12 @@ router.get('/executions/:id',
(req, res, next) => reportsController.findExecutionById(req, res, next)
);
// Export execution results
router.post('/executions/:id/export',
requireRoles('admin', 'manager', 'accountant', 'super_admin'),
(req, res, next) => reportsController.exportExecution(req, res, next)
);
// ============================================================================
// SCHEDULES
// ============================================================================

View File

@ -459,12 +459,21 @@ class OrdersService {
values.push(dto.analytic_account_id);
}
// Recalculate amounts
const subtotal = quantity * priceUnit;
const discountAmount = subtotal * discount / 100;
const amountUntaxed = subtotal - discountAmount;
const amountTax = 0; // TODO: Calculate taxes
const amountTotal = amountUntaxed + amountTax;
// Recalculate amounts using taxesService
const taxIds = dto.tax_ids ?? existingLine.tax_ids;
const taxResult = await taxesService.calculateTaxes(
{
quantity,
priceUnit,
discount,
taxIds,
},
tenantId,
'sales'
);
const amountUntaxed = taxResult.amountUntaxed;
const amountTax = taxResult.amountTax;
const amountTotal = taxResult.amountTotal;
updateFields.push(`amount_untaxed = $${paramIndex++}`);
values.push(amountUntaxed);

View File

@ -1,6 +1,8 @@
import { query, queryOne, getClient } from '../../config/database.js';
import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js';
import { taxesService } from '../financial/taxes.service.js';
import { emailService } from '../../shared/services/email.service.js';
import { logger } from '../../shared/utils/logger.js';
export interface QuotationLine {
id: string;
@ -409,12 +411,21 @@ class QuotationsService {
values.push(dto.tax_ids);
}
// Recalculate amounts
const subtotal = quantity * priceUnit;
const discountAmount = subtotal * discount / 100;
const amountUntaxed = subtotal - discountAmount;
const amountTax = 0; // TODO: Calculate taxes
const amountTotal = amountUntaxed + amountTax;
// Recalculate amounts using taxesService
const taxIds = dto.tax_ids ?? existingLine.tax_ids;
const taxResult = await taxesService.calculateTaxes(
{
quantity,
priceUnit,
discount,
taxIds,
},
tenantId,
'sales'
);
const amountUntaxed = taxResult.amountUntaxed;
const amountTax = taxResult.amountTax;
const amountTotal = taxResult.amountTotal;
updateFields.push(`amount_untaxed = $${paramIndex++}`);
values.push(amountUntaxed);
@ -475,11 +486,257 @@ class QuotationsService {
[userId, id, tenantId]
);
// TODO: Send email notification
// Send email notification to partner
await this.sendQuotationEmail(quotation, tenantId);
return this.findById(id, tenantId);
}
/**
* Send quotation email to partner
*/
private async sendQuotationEmail(quotation: Quotation, tenantId: string): Promise<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>&copy; ${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 }> {
const quotation = await this.findById(id, tenantId);

View File

@ -3,6 +3,8 @@ import jwt from 'jsonwebtoken';
import { config } from '../../config/index.js';
import { AuthenticatedRequest, JwtPayload, UnauthorizedError, ForbiddenError } from '../types/index.js';
import { logger } from '../utils/logger.js';
import { permissionsService } from '../../modules/roles/permissions.service.js';
import { permissionCacheService } from '../../modules/auth/services/permission-cache.service.js';
// Re-export AuthenticatedRequest for convenience
export { AuthenticatedRequest } from '../types/index.js';
@ -73,15 +75,63 @@ export function requirePermission(resource: string, action: string) {
throw new UnauthorizedError('Usuario no autenticado');
}
const { userId, tenantId, roles } = req.user;
// Superusers bypass permission checks
if (req.user.roles.includes('super_admin')) {
if (roles.includes('super_admin')) {
return next();
}
// TODO: Check permission in database
// For now, we'll implement this when we have the permission checking service
logger.debug('Permission check', {
userId: req.user.userId,
// Build permission key for cache lookup
const permissionKey = `${resource}:${action}`;
// Try cache first for fast permission check
const cachedHasPermission = await permissionCacheService.hasPermission(userId, permissionKey);
// If cache hit and user has permission, allow access
if (cachedHasPermission) {
logger.debug('Permission granted (cache hit)', {
userId,
resource,
action,
});
return next();
}
// Cache miss or no permission in cache - check database
const hasPermission = await permissionsService.hasPermission(
tenantId,
userId,
resource,
action
);
if (!hasPermission) {
logger.warn('Access denied - insufficient permissions', {
userId,
tenantId,
resource,
action,
});
throw new ForbiddenError(`No tiene permisos para ${action} en ${resource}`);
}
// Permission granted - update cache with user's effective permissions
// This ensures future checks are faster
try {
const effectivePermissions = await permissionsService.getEffectivePermissions(tenantId, userId);
const permissionKeys = effectivePermissions.map(p => `${p.resource}:${p.action}`);
await permissionCacheService.setUserPermissions(userId, permissionKeys);
} catch (cacheError) {
// Don't fail the request if cache update fails
logger.warn('Failed to update permission cache', {
userId,
error: (cacheError as Error).message,
});
}
logger.debug('Permission granted (database check)', {
userId,
resource,
action,
});

View File

@ -6,3 +6,9 @@ export {
BaseServiceConfig,
} from './base.service.js';
export { emailService, EmailOptions, EmailResult } from './email.service.js';
export {
cacheService,
cacheKeys,
cacheTTL,
CacheOptions,
} from './cache.service.js';

View 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*

View 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*

View 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*

View 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*

View File

@ -1,9 +1,39 @@
---
id: VISION-ERP-CORE
title: Vision General - ERP Core
type: Vision
status: Published
priority: P0
version: 2.0.0
created_date: 2025-12-01
updated_date: 2026-01-10
---
# Vision General: ERP Core
## Resumen Ejecutivo
ERP Core es la **base generica reutilizable** que proporciona el 60-70% del codigo compartido para todas las verticales del ERP Suite. Es una adaptacion de los patrones de Odoo al stack TypeScript/Node.js/React.
**Capacidades Core:**
- Multi-tenancy con Row-Level Security (RLS)
- Autenticacion/Autorizacion avanzada (JWT, OAuth, RBAC)
- Modulos de negocio genericos (Inventario, Ventas, Compras, CRM)
**Capacidades SaaS (Plataforma):**
- Billing y suscripciones con Stripe
- Planes con feature gating y limites
- Notificaciones multicanal (Email, Push, WhatsApp)
- Webhooks outbound con firma HMAC
- Feature flags por tenant/usuario
**Capacidades IA (Inteligencia):**
- Integracion LLM multi-proveedor (OpenRouter)
- MCP Server para herramientas de negocio
- WhatsApp Business con IA conversacional
- Prediccion de inventario y demanda
- Asistente de negocio inteligente
---
## Proposito
@ -19,6 +49,11 @@ Desarrollar ERPs verticales desde cero es costoso y repetitivo. El 60-70% de la
- Ventas y compras
- Contabilidad basica
**Ademas, las plataformas modernas requieren:**
- Modelo de negocio SaaS con suscripciones
- Inteligencia artificial integrada
- Comunicacion omnicanal con clientes
### Solucion
ERP Core provee esta funcionalidad comun de forma:
@ -26,6 +61,8 @@ ERP Core provee esta funcionalidad comun de forma:
- **Extensible:** Las verticales pueden extender sin modificar
- **Multi-tenant:** Aislamiento por tenant desde el diseno
- **Documentado:** Documentacion antes de desarrollo
- **SaaS-Ready:** Billing, planes y self-service incluidos
- **AI-First:** IA integrada en flujos de negocio
---
@ -35,16 +72,24 @@ ERP Core provee esta funcionalidad comun de forma:
1. Completar modulos core: Auth, Users, Roles, Tenants
2. Implementar Partners y Products
3. Establecer patrones de extension para verticales
4. **Integrar Billing y Plans (Stripe)**
5. **Implementar Feature Flags**
### Mediano Plazo (6 meses)
1. Completar Sales, Purchases, Inventory
2. Implementar Financial basico
3. Primera vertical (Construccion) usando el core
4. **Integrar Notificaciones multicanal**
5. **Implementar Webhooks outbound**
6. **Integrar MCP Server y LLM**
### Largo Plazo (12 meses)
1. Todas las verticales usando el core
2. SaaS layer para autocontratacion
2. **SaaS layer completo para autocontratacion**
3. Marketplace de extensiones
4. **WhatsApp Business con IA integrada**
5. **Prediccion de inventario y demanda**
6. **Asistente de negocio inteligente**
---
@ -53,55 +98,100 @@ ERP Core provee esta funcionalidad comun de forma:
### Modelo de Capas
```
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND │
│ React 18 + TypeScript + Tailwind + Zustand │
├─────────────────────────────────────────────────────────────┤
│ API REST │
│ Express.js + TypeScript + Swagger │
├─────────────────────────────────────────────────────────────┤
│ BACKEND │
│ Modulos: Auth | Users | Partners | Products | Sales... │
│ Services + Controllers + DTOs + Entities │
├─────────────────────────────────────────────────────────────┤
│ DATABASE │
│ PostgreSQL 15+ con RLS (Row-Level Security) │
│ Schemas: core_auth | core_partners | core_products... │
└─────────────────────────────────────────────────────────────┘
+------------------------------------------------------------------+
| CLIENTES |
| +----------+ +----------+ +----------+ +----------+ |
| | Web App | |Mobile App| | WhatsApp | | API Rest | |
| | React 18 | | Expo 51 | | Meta API | | Swagger | |
| +----------+ +----------+ +----------+ +----------+ |
+------------------------------------------------------------------+
| CAPA IA |
| +----------------------------------------------------------+ |
| | MCP SERVER | |
| | (Model Context Protocol - Herramientas de Negocio) | |
| +----------------------------------------------------------+ |
| | LLM GATEWAY (OpenRouter) | |
| | Claude | GPT-4 | Gemini | Mistral | Llama | |
| +----------------------------------------------------------+ |
+------------------------------------------------------------------+
| API GATEWAY |
| +----------------------------------------------------------+ |
| | Express.js + TypeScript + Swagger | |
| | Middleware: Auth | Tenant | Rate Limit | Validation | |
| +----------------------------------------------------------+ |
+------------------------------------------------------------------+
| SERVICIOS BACKEND |
| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ |
| | Auth | | Users | |Tenants| |Billing| | Plans | |Webhooks| |
| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ |
| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ |
| |Notif | |Storage| | Audit | |FFlags | | AI | |WhatsApp| |
| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ |
| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ |
| |Catalog| |Invntry| | Sales | |Purchas| | CRM | |Projects| |
| +-------+ +-------+ +-------+ +-------+ +-------+ +-------+ |
+------------------------------------------------------------------+
| BASE DE DATOS |
| +----------------------------------------------------------+ |
| | PostgreSQL 16+ con RLS (Row-Level Security) | |
| | Schemas: auth | users | tenants | billing | plans | ai | |
| | notifications | webhooks | storage | audit | |
| | core_catalog | core_inventory | core_sales | |
| +----------------------------------------------------------+ |
+------------------------------------------------------------------+
| SERVICIOS EXTERNOS |
| +--------+ +--------+ +--------+ +--------+ +--------+ |
| | Stripe | |SendGrid| | S3/R2 | | Redis | |OpenRouter| |
| +--------+ +--------+ +--------+ +--------+ +--------+ |
| +--------+ +--------+ |
| |WhatsApp| | Vision | |
| +--------+ +--------+ |
+------------------------------------------------------------------+
```
### Modelo de Extension
```
┌─────────────────────────────────────────────────────────────┐
│ ERP CORE │
│ Modulos Genericos (MGN-001 a MGN-015) │
│ 60-70% funcionalidad comun │
└────────────────────────┬────────────────────────────────────┘
│ EXTIENDE
┌────────────────┼────────────────┐
↓ ↓ ↓
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Construccion │ │Vidrio Templado│ │ Retail │
│ (MAI-*) │ │ (MVT-*) │ │ (MRT-*) │
│ 30-40% extra │ │ 30-40% extra │ │ 30-40% extra │
└───────────────┘ └───────────────┘ └───────────────┘
+------------------------------------------------------------------+
| ERP CORE |
| Modulos Genericos (MGN-001 a MGN-022) |
| 60-70% funcionalidad comun |
| |
| +-- Core Business: auth, users, tenants, catalog, inventory |
| +-- SaaS Layer: billing, plans, webhooks, feature-flags |
| +-- IA Layer: ai-integration, whatsapp, mcp-server |
+------------------------------------------------------------------+
| EXTIENDE
+----------------+----------------+
| | |
+---------------+ +---------------+ +---------------+
| Construccion | |Vidrio Templado| | Retail |
| (MAI-*) | | (MVT-*) | | (MRT-*) |
| 30-40% extra | | 30-40% extra | | 30-40% extra |
+---------------+ +---------------+ +---------------+
```
---
## Modulos Core (MGN-*)
### Fase Foundation (P0)
| Codigo | Modulo | Descripcion | Prioridad | Estado |
|--------|--------|-------------|-----------|--------|
| MGN-001 | auth | Autenticacion JWT, OAuth, sessions | P0 | En desarrollo |
| MGN-002 | users | Gestion de usuarios CRUD | P0 | En desarrollo |
| MGN-003 | roles | Roles y permisos (RBAC) | P0 | Planificado |
| MGN-004 | tenants | Multi-tenancy, aislamiento | P0 | Planificado |
| MGN-004 | tenants | Multi-tenancy, aislamiento RLS | P0 | Planificado |
### Fase Core Business (P1)
| Codigo | Modulo | Descripcion | Prioridad | Estado |
|--------|--------|-------------|-----------|--------|
| MGN-005 | catalogs | Catalogos maestros genericos | P1 | Planificado |
| MGN-006 | settings | Configuracion del sistema | P1 | Planificado |
| MGN-007 | audit | Auditoria y logs | P1 | Planificado |
| MGN-008 | notifications | Sistema de notificaciones | P2 | Planificado |
| MGN-008 | notifications | Sistema de notificaciones multicanal | P1 | Planificado |
| MGN-009 | reports | Reportes genericos | P2 | Planificado |
| MGN-010 | financial | Contabilidad basica | P1 | Planificado |
| MGN-011 | inventory | Inventario basico | P1 | Planificado |
@ -110,47 +200,174 @@ ERP Core provee esta funcionalidad comun de forma:
| MGN-014 | crm | CRM basico | P2 | Planificado |
| MGN-015 | projects | Proyectos genericos | P2 | Planificado |
### Fase SaaS Platform (P3)
| Codigo | Modulo | Descripcion | Prioridad | Estado |
|--------|--------|-------------|-----------|--------|
| MGN-016 | billing | Suscripciones y pagos (Stripe) | P3 | Planificado |
| MGN-017 | plans | Planes, limites y feature gating | P3 | Planificado |
| MGN-018 | webhooks | Webhooks outbound con firma HMAC | P3 | Planificado |
| MGN-019 | feature-flags | Feature flags por tenant/usuario | P3 | Planificado |
### Fase IA Intelligence (P3)
| Codigo | Modulo | Descripcion | Prioridad | Estado |
|--------|--------|-------------|-----------|--------|
| MGN-020 | ai-integration | Gateway LLM multi-proveedor | P3 | Planificado |
| MGN-021 | whatsapp-business | WhatsApp Business con IA | P3 | Planificado |
| MGN-022 | mcp-server | MCP Server para herramientas | P3 | Planificado |
---
## Alcance SaaS (Plataforma)
> Ver detalles completos en [ARQUITECTURA-SAAS.md](ARQUITECTURA-SAAS.md)
### Billing y Suscripciones (MGN-016)
**Proveedor:** Stripe
**Caracteristicas:**
- Suscripciones recurrentes (mensual/anual)
- Trial gratuito configurable
- Upgrade/downgrade con prorateo automatico
- Facturas y recibos
- Webhooks Stripe sincronizados
- Portal de cliente Stripe integrado
### Planes y Limites (MGN-017)
| Plan | Precio | Usuarios | Storage | Features |
|------|--------|----------|---------|----------|
| Free | $0/mes | 1 | 100MB | Core basico |
| Starter | $29/mes | 5 | 1GB | + API access |
| Pro | $79/mes | 20 | 10GB | + AI assistant + Webhooks |
| Enterprise | $199/mes | Unlimited | Unlimited | + Custom branding + SLA |
### Notificaciones Multicanal (MGN-008)
**Canales:**
- Email (SendGrid, AWS SES, SMTP)
- Push Web (Web Push API con VAPID)
- In-App (WebSocket real-time)
- WhatsApp Business (Meta Cloud API)
### Webhooks Outbound (MGN-018)
**Eventos:** user.*, subscription.*, invoice.*, tenant.*
**Seguridad:** Firma HMAC-SHA256, reintentos exponenciales
### Feature Flags (MGN-019)
- Flags por tenant y por usuario
- Rollout gradual
- A/B testing
- Evaluaciones por contexto
---
## Alcance IA (Inteligencia)
> Ver detalles completos en [ARQUITECTURA-IA.md](ARQUITECTURA-IA.md)
### Gateway LLM (MGN-020)
**Gateway:** OpenRouter (acceso a 50+ modelos)
| Modelo | Costo/1M tokens | Uso |
|--------|-----------------:|-----|
| Claude 3 Haiku | $0.25 | Default |
| Claude 3 Sonnet | $3.00 | Premium |
| GPT-4o-mini | $0.15 | Fallback |
| Mistral 7B | $0.06 | Economico |
**Caracteristicas:**
- Cambio de modelo sin modificar codigo
- Fallback automatico entre modelos
- Token tracking por tenant
- Configuracion por tenant
### MCP Server (MGN-022)
**Herramientas de Negocio:**
- **Productos:** list, details, availability
- **Inventario:** stock, low-stock, movements
- **Ventas:** create_order, status, update
- **Clientes:** search, balance
- **Fiados:** balance, create, payment
### WhatsApp Business (MGN-021)
**Flujo:** Cliente -> WhatsApp -> LLM -> MCP -> Respuesta
**Tipos de mensaje:** texto, audio, imagen, ubicacion
### Prediccion de Inventario
**Algoritmos:**
- Demanda: Promedio movil ponderado (4 semanas)
- Reorden: (Demanda_diaria x Lead_time) + Stock_seguridad
- Dias de inventario: Stock_actual / Demanda_diaria
---
## Integraciones Externas
> Ver catalogo completo en [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md)
| Categoria | Servicio | Modulo | Uso |
|-----------|----------|--------|-----|
| Pagos | Stripe | MGN-016 | Billing y suscripciones |
| Email | SendGrid/SES | MGN-008 | Notificaciones |
| Push | Web Push API | MGN-008 | Notificaciones |
| WhatsApp | Meta Cloud API | MGN-021 | Mensajeria |
| Storage | S3/R2/MinIO | - | Archivos |
| Cache | Redis/BullMQ | - | Cache y colas |
| LLM | OpenRouter | MGN-020 | Inteligencia artificial |
---
## Stack Tecnologico
> Ver detalle completo en [STACK-TECNOLOGICO.md](STACK-TECNOLOGICO.md)
### Backend
| Tecnologia | Version | Proposito |
|------------|---------|-----------|
| Node.js | 20+ | Runtime |
| Express.js | 4.x | Framework HTTP |
| TypeScript | 5.3+ | Lenguaje |
| TypeORM | 0.3.17 | ORM |
| JWT + bcryptjs | - | Autenticacion |
| Zod, class-validator | - | Validacion |
| Swagger | 3.x | Documentacion API |
| Jest | 29.x | Testing |
| BullMQ | 5.x | Colas asincronas |
| Socket.io | 4.x | WebSocket |
| Stripe SDK | Latest | Billing |
### Frontend
| Tecnologia | Version | Proposito |
|------------|---------|-----------|
| React | 18.x | Framework UI |
| Vite | 5.x | Build tool |
| TypeScript | 5.3+ | Lenguaje |
| Zustand | 4.x | State management |
| Tailwind CSS | 4.x | Styling |
| React Query | 5.x | Data fetching |
| React Hook Form | 7.x | Formularios |
### Database
| Tecnologia | Version | Proposito |
|------------|---------|-----------|
| PostgreSQL | 15+ | Motor BD |
| RLS | - | Row-Level Security |
| uuid-ossp | - | Generacion UUIDs |
| pg_trgm | - | Busqueda fuzzy |
| PostgreSQL | 16+ | Base de datos |
| RLS | - | Multi-tenancy |
| Redis | 7.x | Cache y colas |
---
## Principios de Diseno
### 1. Multi-Tenancy First
Toda tabla tiene `tenant_id`. Todo query filtra por tenant.
Toda tabla tiene `tenant_id`. Todo query filtra por tenant via RLS.
### 2. Documentation Driven
Documentar antes de desarrollar. La documentacion es el contrato.
@ -164,11 +381,20 @@ Adaptar patrones probados de Odoo al stack TypeScript.
### 5. Single Source of Truth
Un lugar para cada dato. Sincronizacion automatica.
### 6. SaaS by Design
Billing, planes y self-service desde el inicio.
### 7. AI-First Architecture
IA integrada en flujos de negocio, no como addon.
### 8. Event-Driven Communication
Webhooks y eventos para comunicacion entre sistemas.
---
## Entregables por Fase
### Fase 1: Foundation (Actual)
### Fase 1: Foundation (En progreso)
- [ ] MGN-001 Auth completo
- [ ] MGN-002 Users completo
- [ ] MGN-003 Roles completo
@ -185,26 +411,46 @@ Un lugar para cada dato. Sincronizacion automatica.
### Fase 3: Extended
- [ ] MGN-006 Settings
- [ ] MGN-007 Audit
- [ ] MGN-008 Notifications
- [ ] MGN-008 Notifications (multicanal)
- [ ] MGN-009 Reports
- [ ] MGN-014 CRM
- [ ] MGN-015 Projects
### Fase 4: SaaS Platform
- [ ] MGN-016 Billing (Stripe)
- [ ] MGN-017 Plans
- [ ] MGN-018 Webhooks
- [ ] MGN-019 Feature Flags
- [ ] Portal de autocontratacion
### Fase 5: IA Intelligence
- [ ] MGN-020 AI Integration (OpenRouter)
- [ ] MGN-021 WhatsApp Business
- [ ] MGN-022 MCP Server
- [ ] Prediccion de inventario
- [ ] Asistente de negocio inteligente
---
## Referencias
| Recurso | Path |
|---------|------|
| Arquitectura SaaS | [ARQUITECTURA-SAAS.md](ARQUITECTURA-SAAS.md) |
| Arquitectura IA | [ARQUITECTURA-IA.md](ARQUITECTURA-IA.md) |
| Integraciones | [INTEGRACIONES-EXTERNAS.md](INTEGRACIONES-EXTERNAS.md) |
| Stack Tecnologico | [STACK-TECNOLOGICO.md](STACK-TECNOLOGICO.md) |
| Directivas | `orchestration/directivas/` |
| Patrones Odoo | `orchestration/directivas/DIRECTIVA-PATRONES-ODOO.md` |
| Templates | `orchestration/templates/` |
| Catálogo central | `shared/catalog/` *(patrones reutilizables)* |
| Template SaaS | `projects/template-saas/` |
| MiChangarrito | `projects/michangarrito/` |
---
## Metricas de Exito
### Metricas Core
| Metrica | Objetivo |
|---------|----------|
| Cobertura de tests | >80% |
@ -212,6 +458,24 @@ Un lugar para cada dato. Sincronizacion automatica.
| Reutilizacion en verticales | >60% |
| Tiempo de setup nueva vertical | <1 semana |
### Metricas SaaS
| Metrica | Objetivo |
|---------|----------|
| Tiempo de onboarding self-service | <5 minutos |
| Conversion trial a paid | >10% |
| Churn mensual | <5% |
| Uptime SLA | >99.5% |
### Metricas IA
| Metrica | Objetivo |
|---------|----------|
| Precision de respuestas IA | >85% |
| Latencia promedio LLM | <2 segundos |
| Adopcion de chat IA | >50% usuarios activos |
| Precision prediccion inventario | >70% |
---
*Ultima actualizacion: Diciembre 2025*
*Ultima actualizacion: Enero 2026*

View 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*

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -4,14 +4,15 @@
| Metrica | Valor |
|---------|-------|
| Total Modulos | 19 |
| Total Modulos | 22 |
| Modulos P0 (Criticos) | 4 |
| Modulos P1 (Core) | 6 |
| Modulos P2 (Extended) | 5 |
| Modulos P3 (SaaS) | 4 |
| Modulos P3 (SaaS Platform) | 4 |
| Modulos P3 (IA Intelligence) | 3 |
| Completados | 0 |
| En Desarrollo | 2 |
| Planificados | 17 |
| Planificados | 20 |
---
@ -52,9 +53,17 @@
| Codigo | Modulo | Estado | Progreso | Docs |
|--------|--------|--------|----------|------|
| MGN-016 | [billing](#mgn-016-billing) | Planificado | 0% | [Ver](./MGN-016-billing/) |
| MGN-017 | [payments-pos](#mgn-017-payments-pos) | Planificado | 0% | [Ver](./MGN-017-payments-pos/) |
| MGN-018 | [whatsapp-business](#mgn-018-whatsapp-business) | Planificado | 0% | [Ver](./MGN-018-whatsapp-business/) |
| MGN-019 | [ai-agents](#mgn-019-ai-agents) | Planificado | 0% | [Ver](./MGN-019-ai-agents/) |
| MGN-017 | [plans](#mgn-017-plans) | Planificado | 0% | [Ver](./MGN-017-plans/) |
| MGN-018 | [webhooks](#mgn-018-webhooks) | Planificado | 0% | [Ver](./MGN-018-webhooks/) |
| MGN-019 | [feature-flags](#mgn-019-feature-flags) | Planificado | 0% | [Ver](./MGN-019-feature-flags/) |
### P3 - IA Intelligence
| Codigo | Modulo | Estado | Progreso | Docs |
|--------|--------|--------|----------|------|
| MGN-020 | [ai-integration](#mgn-020-ai-integration) | Planificado | 0% | [Ver](./MGN-020-ai-integration/) |
| MGN-021 | [whatsapp-business](#mgn-021-whatsapp-business) | Planificado | 0% | [Ver](./MGN-021-whatsapp-business/) |
| MGN-022 | [mcp-server](#mgn-022-mcp-server) | Planificado | 0% | [Ver](./MGN-022-mcp-server/) |
---
@ -376,98 +385,204 @@
### MGN-016: Billing
**Proposito:** Facturacion SaaS por asientos (per-seat billing)
**Proposito:** Suscripciones y pagos SaaS con Stripe
| Aspecto | Detalle |
|---------|---------|
| Schema BD | `billing` |
| Tablas | tenant_owners, payment_methods, invoices, invoice_lines, payments, coupons, coupon_redemptions, usage_records, subscription_history |
| Tablas | subscriptions, invoices, payments, payment_methods |
| Endpoints | 15+ |
| Dependencias | MGN-001, MGN-004 Tenants |
| Usado por | MGN-017, MGN-018, MGN-019 |
| Dependencias | MGN-001 Auth, MGN-004 Tenants |
| Usado por | MGN-017 Plans |
| Integracion | Stripe |
**Funcionalidades:**
- Suscripciones mensuales por tenant
- Modelo per-seat: base + extra seats × precio
- Feature flags por plan (Starter, Growth, Enterprise)
- Gestion de propietarios de tenant
- Cupones y descuentos
- Historial de cambios de suscripcion
- Facturacion automatica
- Metodos de pago (tarjetas, SPEI)
- Suscripciones recurrentes (mensual/anual)
- Trial gratuito configurable
- Upgrade/downgrade con prorateo
- Webhooks Stripe sincronizados
- Portal de cliente Stripe integrado
- Facturas y recibos automaticos
- Multiples monedas (USD, MXN)
---
### MGN-017: Payments POS
### MGN-017: Plans
**Proposito:** Integraciones con terminales de pago (MercadoPago, Clip)
**Proposito:** Planes, limites y feature gating
| Aspecto | Detalle |
|---------|---------|
| Schema BD | `integrations` |
| Tablas | payment_providers, payment_credentials, payment_terminals, payment_transactions, refunds, webhook_logs, reconciliation_batches |
| Endpoints | 20+ |
| Dependencias | MGN-001, MGN-004, MGN-010 Financial |
| Usado por | Verticales POS |
| Schema BD | `plans` |
| Tablas | plans, plan_features, tenant_limits |
| Endpoints | 10+ |
| Dependencias | MGN-016 Billing |
| Usado por | Todos los modulos |
**Funcionalidades:**
- Multi-provider: MercadoPago (OAuth) y Clip (API Keys)
- Registro de terminales por ubicacion
- Procesamiento de transacciones
- Reembolsos parciales y totales
- Webhooks para notificaciones
- Conciliacion de transacciones
- Generacion de asientos contables
- Manejo de comisiones por provider
- Planes Free, Starter, Pro, Enterprise
- Feature gating por plan
- Limites numericos (usuarios, storage, etc.)
- Verificacion de features en tiempo real
- Upgrade paths configurables
**Planes propuestos:**
| Plan | Precio | Usuarios | Storage | Features |
|------|--------|----------|---------|----------|
| Free | $0/mes | 1 | 100MB | Core basico |
| Starter | $29/mes | 5 | 1GB | + API access |
| Pro | $79/mes | 20 | 10GB | + AI + Webhooks |
| Enterprise | $199/mes | Unlimited | Unlimited | + Custom + SLA |
---
### MGN-018: WhatsApp Business
### MGN-018: Webhooks
**Proposito:** Integracion con WhatsApp Business Cloud API
**Proposito:** Webhooks outbound con firma HMAC
| Aspecto | Detalle |
|---------|---------|
| Schema BD | `messaging` |
| Tablas | whatsapp_accounts, whatsapp_templates, whatsapp_conversations, whatsapp_messages, chatbot_flows, chatbot_nodes, chatbot_sessions, whatsapp_campaigns |
| Endpoints | 25+ |
| Dependencias | MGN-001, MGN-004, MGN-005 Catalogs |
| Usado por | MGN-019 AI Agents |
| Schema BD | `webhooks` |
| Tablas | webhook_endpoints, webhook_events, webhook_deliveries |
| Endpoints | 8+ |
| Dependencias | MGN-001 Auth, MGN-004 Tenants |
| Usado por | Integraciones externas |
| Cola | BullMQ (Redis) |
**Funcionalidades:**
- Cuentas de WhatsApp Business por tenant
- Templates de mensajes (HSM) aprobados por Meta
- Conversaciones bidireccionales
- Chatbots con flujos visuales
- Campañas de marketing masivo
- Opt-in/opt-out de contactos
- Metricas de entrega y lectura
- Webhooks de WhatsApp Cloud API
- Registro de endpoints por tenant
- Firma HMAC-SHA256 en cada request
- Politica de reintentos exponencial
- Log de entregas y respuestas
- Eventos: user.*, subscription.*, invoice.*
**Eventos disponibles:**
| Evento | Descripcion |
|--------|-------------|
| user.created | Usuario creado |
| subscription.created | Suscripcion creada |
| subscription.cancelled | Suscripcion cancelada |
| invoice.paid | Factura pagada |
---
### MGN-019: AI Agents
### MGN-019: Feature Flags
**Proposito:** Agentes inteligentes con RAG para automatizacion
**Proposito:** Feature flags por tenant y usuario
| Aspecto | Detalle |
|---------|---------|
| Schema BD | `ai_agents` |
| Tablas | agents, knowledge_bases, agent_knowledge_bases, kb_documents, kb_chunks, tool_definitions, agent_tools, conversations, messages, tool_executions, feedback, usage_logs |
| Endpoints | 30+ |
| Dependencias | MGN-001, MGN-004, MGN-018 WhatsApp |
| Schema BD | `feature_flags` |
| Tablas | flags, tenant_flags, user_flags |
| Endpoints | 6+ |
| Dependencias | MGN-001 Auth, MGN-004 Tenants |
| Usado por | Todos los modulos |
**Funcionalidades:**
- Flags globales con valor default
- Override por tenant
- Override por usuario
- Rollout gradual (porcentaje)
- A/B testing
- Evaluacion por contexto
---
### MGN-020: AI Integration
**Proposito:** Gateway LLM multi-proveedor (OpenRouter)
| Aspecto | Detalle |
|---------|---------|
| Schema BD | `ai` |
| Tablas | ai_conversations, ai_messages, ai_usage_logs |
| Endpoints | 10+ |
| Dependencias | MGN-001 Auth, MGN-004 Tenants |
| Usado por | MGN-021 WhatsApp Business |
| Integracion | OpenRouter |
**Funcionalidades:**
- Acceso a 50+ modelos LLM
- Cambio de modelo sin codigo
- Fallback automatico entre modelos
- Token tracking por tenant
- Rate limiting por plan
- Configuracion por tenant
**Modelos soportados:**
| Modelo | Costo/1M tokens | Uso |
|--------|-----------------:|-----|
| Claude 3 Haiku | $0.25 | Default |
| Claude 3 Sonnet | $3.00 | Premium |
| GPT-4o-mini | $0.15 | Fallback |
| Mistral 7B | $0.06 | Economico |
---
### MGN-021: WhatsApp Business
**Proposito:** WhatsApp Business con IA conversacional
| Aspecto | Detalle |
|---------|---------|
| Schema BD | `whatsapp` |
| Tablas | whatsapp_sessions, whatsapp_messages, whatsapp_templates |
| Endpoints | 15+ |
| Dependencias | MGN-020 AI Integration |
| Usado por | Verticales |
| Integracion | Meta WhatsApp Cloud API |
**Funcionalidades:**
- Agentes configurables por tenant
- Knowledge bases con documentos
- RAG con pgvector (embeddings 1536 dims)
- Definicion de tools/funciones
- Conversaciones con historial
- Ejecucion de tools en tiempo real
- Feedback de usuarios (thumbs up/down)
- Metricas de uso y tokens
- Integracion con WhatsApp como canal
- Webhook receiver Meta Cloud API
- Procesamiento de mensajes (texto, audio, imagen)
- Transcripcion de audio (Whisper)
- OCR de imagenes (Google Vision)
- IA conversacional con contexto
- Templates HSM pre-aprobados
- Envio de mensajes outbound
**Flujo de mensaje:**
```
Cliente -> WhatsApp -> Webhook -> LLM -> MCP -> Respuesta
```
---
### MGN-022: MCP Server
**Proposito:** Model Context Protocol Server para herramientas de negocio
| Aspecto | Detalle |
|---------|---------|
| Schema BD | N/A (usa otros schemas) |
| Endpoints | N/A (Protocolo MCP) |
| Dependencias | MGN-020 AI Integration |
| Usado por | Clientes MCP (Claude Desktop, IA conversacional) |
| Protocolo | MCP (Anthropic) |
| Puerto | 3142 |
**Funcionalidades:**
- Servidor MCP standalone
- Herramientas de negocio expuestas a LLMs
- Contexto multi-tenant
- Autenticacion por API key
**Herramientas disponibles:**
| Tool | Descripcion |
|------|-------------|
| product_list | Listar productos |
| product_details | Detalles de producto |
| product_availability | Disponibilidad |
| inventory_stock | Consultar stock |
| inventory_low_stock | Alertas stock bajo |
| sales_create_order | Crear orden |
| sales_order_status | Estado de orden |
| customer_search | Buscar cliente |
| fiado_balance | Balance fiados |
---
@ -482,15 +597,13 @@ Fase 1: Foundation
Fase 2: Core Business
├── MGN-005 Catalogs
├── Partners (parte de Catalogs)
├── Products (parte de Catalogs)
├── MGN-006 Settings
├── MGN-010 Financial
├── MGN-011 Inventory
├── MGN-012 Purchasing
├── MGN-013 Sales
└── MGN-010 Financial
└── MGN-013 Sales
Fase 3: Extended
├── MGN-006 Settings
├── MGN-007 Audit
├── MGN-008 Notifications
├── MGN-009 Reports
@ -499,9 +612,14 @@ Fase 3: Extended
Fase 4: SaaS Platform
├── MGN-016 Billing (depende de MGN-004)
├── MGN-017 Payments POS (depende de MGN-010)
├── MGN-018 WhatsApp Business (depende de MGN-005)
└── MGN-019 AI Agents (depende de MGN-018)
├── MGN-017 Plans (depende de MGN-016)
├── MGN-018 Webhooks (depende de MGN-004)
└── MGN-019 Feature Flags (depende de MGN-004)
Fase 5: IA Intelligence
├── MGN-020 AI Integration (depende de MGN-004)
├── MGN-021 WhatsApp Business (depende de MGN-020)
└── MGN-022 MCP Server (depende de MGN-020)
```
---
@ -510,50 +628,86 @@ Fase 4: SaaS Platform
```mermaid
graph TD
MGN001[MGN-001 Auth] --> MGN002[MGN-002 Users]
MGN001 --> MGN004[MGN-004 Tenants]
MGN002 --> MGN003[MGN-003 Roles]
MGN004 --> MGN005[MGN-005 Catalogs]
MGN005 --> MGN010[MGN-010 Financial]
MGN005 --> MGN011[MGN-011 Inventory]
MGN011 --> MGN012[MGN-012 Purchasing]
MGN011 --> MGN013[MGN-013 Sales]
MGN010 --> MGN012
MGN010 --> MGN013
MGN005 --> MGN014[MGN-014 CRM]
MGN002 --> MGN015[MGN-015 Projects]
subgraph "Fase 1: Foundation"
MGN001[MGN-001 Auth]
MGN002[MGN-002 Users]
MGN003[MGN-003 Roles]
MGN004[MGN-004 Tenants]
end
%% SaaS Platform Modules
MGN004 --> MGN016[MGN-016 Billing]
MGN010 --> MGN017[MGN-017 Payments POS]
MGN005 --> MGN018[MGN-018 WhatsApp]
MGN018 --> MGN019[MGN-019 AI Agents]
subgraph "Fase 2-3: Core/Extended"
MGN005[MGN-005 Catalogs]
MGN010[MGN-010 Financial]
MGN011[MGN-011 Inventory]
MGN008[MGN-008 Notifications]
end
subgraph "Fase 4: SaaS Platform"
MGN016[MGN-016 Billing]
MGN017[MGN-017 Plans]
MGN018[MGN-018 Webhooks]
MGN019[MGN-019 Feature Flags]
end
subgraph "Fase 5: IA Intelligence"
MGN020[MGN-020 AI Integration]
MGN021[MGN-021 WhatsApp Business]
MGN022[MGN-022 MCP Server]
end
MGN001 --> MGN002
MGN001 --> MGN004
MGN002 --> MGN003
MGN004 --> MGN005
MGN005 --> MGN010
MGN005 --> MGN011
%% SaaS Platform
MGN004 --> MGN016
MGN016 --> MGN017
MGN016 --> MGN018
MGN004 --> MGN018
MGN004 --> MGN019
%% IA Intelligence
MGN004 --> MGN020
MGN020 --> MGN021
MGN020 --> MGN022
MGN021 --> MGN022
```
---
## Proximas Acciones
1. **Completar documentacion MGN-001 Auth**
- DDL Specification
- Especificacion Backend
- User Stories
1. **Completar documentacion Fase 1: Foundation**
- MGN-001 Auth: DDL, Backend Spec, User Stories
- MGN-002 Users: DDL, Backend Spec, User Stories
- MGN-003 Roles: RBAC design
- MGN-004 Tenants: RLS implementation
2. **Completar documentacion MGN-002 Users**
- Mismos entregables
2. **Planificar Fase 4: SaaS Platform**
- MGN-016 Billing: Stripe integration, webhooks
- MGN-017 Plans: Feature gating, limits
- MGN-018 Webhooks: HMAC signing, retries
- MGN-019 Feature Flags: Rollout, A/B testing
3. **Iniciar MGN-003 Roles**
- Requerimientos funcionales
- Diseno de RBAC
4. **Planificar Fase 4: SaaS Platform**
- MGN-016 Billing: Per-seat pricing, feature flags
- MGN-017 Payments POS: MercadoPago, Clip
- MGN-018 WhatsApp Business: Cloud API, chatbots
- MGN-019 AI Agents: RAG, pgvector, tools
3. **Planificar Fase 5: IA Intelligence**
- MGN-020 AI Integration: OpenRouter gateway
- MGN-021 WhatsApp Business: Meta Cloud API
- MGN-022 MCP Server: Business tools
---
*Ultima actualizacion: Diciembre 2025*
## Referencias
| Documento | Path |
|-----------|------|
| Vision General | [VISION-ERP-CORE.md](../00-vision-general/VISION-ERP-CORE.md) |
| Arquitectura SaaS | [ARQUITECTURA-SAAS.md](../00-vision-general/ARQUITECTURA-SAAS.md) |
| Arquitectura IA | [ARQUITECTURA-IA.md](../00-vision-general/ARQUITECTURA-IA.md) |
| Integraciones | [INTEGRACIONES-EXTERNAS.md](../00-vision-general/INTEGRACIONES-EXTERNAS.md) |
| Stack Tecnologico | [STACK-TECNOLOGICO.md](../00-vision-general/STACK-TECNOLOGICO.md) |
---
*Ultima actualizacion: Enero 2026*

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -4,7 +4,7 @@
**Nombre:** Reportes y Dashboards
**Fase:** 02 - Core Business
**Story Points:** 35 SP
**Estado:** COMPLETADO (Sprint 8-11)
**Estado:** COMPLETADO (Sprint 8-13)
**Ultima actualizacion:** 2026-01-07
---
@ -221,17 +221,45 @@ Sistema de reportes y dashboards con generador de reportes, exportacion multi-fo
| Sprint 9 | Frontend | Dashboard UI | 24 |
| Sprint 10 | Frontend | Report Builder UI | 13 |
| Sprint 11 | Frontend | Scheduled Reports UI | 11 |
| **TOTAL** | | | **62** |
| Sprint 12 | Frontend | Paginas, Rutas y Navegacion | 5 |
| Sprint 13 | Testing | Unit Tests (56 tests) | 4 |
| **TOTAL** | | | **71** |
---
## Pendientes Futuros
## PDF Export (BE-026 - COMPLETADO)
| Item | Descripcion | Prioridad |
|------|-------------|-----------|
| PDF Export | Integracion con puppeteer para PDF | P2 |
| Tests | Tests unitarios para componentes | P2 |
| Pages | Crear paginas/rutas para features | P1 |
| Componente | Descripcion | Estado |
|------------|-------------|--------|
| PdfService | Servicio Puppeteer para generacion PDF | Implementado |
| ReportTemplates | Templates HTML (tabular, financial, trialBalance) | Implementado |
| ExportService | Actualizado para usar PdfService | Implementado |
| Endpoints Export | /executions/:id/export, /trial-balance/export | Implementado |
| Health Check | /pdf/health para verificar estado | Implementado |
---
## Frontend Pages (Sprint 12)
| Pagina | Path | Descripcion | Estado |
|--------|------|-------------|--------|
| ReportsPage | /reports | Landing page con navegacion | Implementado |
| ReportBuilderPage | /reports/builder | Constructor visual de reportes | Implementado |
| ScheduledReportsPage | /reports/scheduled | Gestion de reportes programados | Implementado |
---
## Unit Tests (Sprint 13)
| Archivo | Feature | Tests | Cobertura |
|---------|---------|------:|-----------|
| CronBuilder.test.tsx | scheduled-reports | 12 | rendering, presets, advanced editor |
| RecipientManager.test.tsx | scheduled-reports | 13 | add/remove, validation, duplicates |
| FilterBuilder.test.tsx | report-builder | 10 | add/remove, operators |
| EntityExplorer.test.tsx | report-builder | 21 | states, categories, search |
| **TOTAL** | | **56** | |
**Suite completa frontend:** 94 tests pasando
---
@ -241,6 +269,6 @@ Ver: [TRACEABILITY.yml](./implementacion/TRACEABILITY.yml)
---
**Actualizado por:** Frontend-Agent (Claude Opus 4.5)
**Actualizado por:** Backend-Agent (Claude Opus 4.5)
**Fecha:** 2026-01-07
**Sprint:** 11 - COMPLETADO
**Tarea:** BE-026 - PDF Export COMPLETADO

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -7,7 +7,7 @@ epic_name: Reports
phase: 2
phase_name: Core Business
story_points: 35
status: completed # Sprint 8-11: Backend + Frontend completo
status: completed # Sprint 8-13: Backend + Frontend + Tests completo
last_updated: "2026-01-07"
# =============================================================================
@ -253,12 +253,37 @@ implementation:
- name: ExportService
file: apps/backend/src/modules/reports/export.service.ts
status: pending
status: completed
requirement: RF-REPORT-001
methods:
- {name: toPdf, description: Exportar a PDF}
- {name: toExcel, description: Exportar a Excel}
- {name: toCsv, description: Exportar a CSV}
- {name: export, description: Exportar datos a formato especificado}
- {name: exportToPdf, description: Exportar a PDF usando PdfService}
- {name: exportToXlsx, description: Exportar a Excel}
- {name: exportToCsv, description: Exportar a CSV}
- {name: exportToJson, description: Exportar a JSON}
- {name: exportToHtml, description: Exportar a HTML}
- name: PdfService
file: apps/backend/src/modules/reports/pdf.service.ts
status: completed
requirement: RF-REPORT-001
methods:
- {name: generateFromHtml, description: Generar PDF desde HTML}
- {name: generateFromUrl, description: Generar PDF desde URL}
- {name: healthCheck, description: Verificar disponibilidad del servicio}
- {name: close, description: Cerrar instancia del navegador}
dependencies:
- {package: puppeteer, version: "^22.15.0", purpose: "Headless browser for PDF generation"}
- name: ReportTemplates
file: apps/backend/src/modules/reports/templates/report-templates.ts
status: completed
requirement: RF-REPORT-001
methods:
- {name: generateTabularReport, description: Generar reporte tabular HTML}
- {name: generateFinancialReport, description: Generar reporte financiero HTML}
- {name: generateTrialBalance, description: Generar balanza de comprobacion HTML}
- {name: generateDashboardExport, description: Generar exportacion de dashboard HTML}
- name: DashboardsService
file: apps/backend/src/modules/reports/dashboards.service.ts
@ -310,9 +335,19 @@ implementation:
description: Ejecutar reporte
requirement: RF-REPORT-001
- method: POST
path: /api/v1/reports/executions/:id/export
description: Exportar resultado de ejecucion
requirement: RF-REPORT-001
- method: GET
path: /api/v1/reports/:id/export/:format
description: Exportar reporte
path: /api/v1/reports/quick/trial-balance/export
description: Exportar balanza de comprobacion
requirement: RF-REPORT-001
- method: GET
path: /api/v1/reports/pdf/health
description: Verificar estado servicio PDF
requirement: RF-REPORT-001
- method: GET
@ -430,6 +465,74 @@ implementation:
- {package: "react-grid-layout", version: "^1.4.4", purpose: "Grid drag & drop"}
- {package: "recharts", version: "^2.10.x", purpose: "Charts"}
pages:
path: frontend/src/pages/reports/
status: completed
files:
- name: ReportsPage
file: ReportsPage.tsx
status: completed
description: "Landing page con cards de navegacion"
- name: ReportBuilderPage
file: ReportBuilderPage.tsx
status: completed
description: "Pagina que integra ReportBuilder component"
- name: ScheduledReportsPage
file: ScheduledReportsPage.tsx
status: completed
description: "Pagina CRUD para reportes programados"
tests:
path: frontend/src/features/
framework: Vitest + Testing Library
status: completed
files:
- name: CronBuilder.test.tsx
file: scheduled-reports/__tests__/CronBuilder.test.tsx
tests: 12
status: completed
coverage:
- rendering
- presets_dropdown
- advanced_editor
- cron_descriptions
- name: RecipientManager.test.tsx
file: scheduled-reports/__tests__/RecipientManager.test.tsx
tests: 13
status: completed
coverage:
- rendering
- adding_recipients
- validation
- duplicate_detection
- removing_recipients
- name: FilterBuilder.test.tsx
file: report-builder/__tests__/FilterBuilder.test.tsx
tests: 10
status: completed
coverage:
- rendering
- adding_filters
- removing_filters
- operator_types
- name: EntityExplorer.test.tsx
file: report-builder/__tests__/EntityExplorer.test.tsx
tests: 21
status: completed
coverage:
- loading_state
- error_state
- category_expansion
- entity_selection
- search_filtering
# =============================================================================
# DEPENDENCIAS
# =============================================================================
@ -458,7 +561,7 @@ dependencies:
metrics:
story_points:
estimated: 35
actual: 35 # Sprint 8: 10, Sprint 9: 10, Sprint 10: 8, Sprint 11: 7
actual: 45 # Sprint 8: 10, Sprint 9: 10, Sprint 10: 8, Sprint 11: 7, Sprint 12: 5, Sprint 13: 5
documentation:
requirements: 4
@ -467,9 +570,10 @@ metrics:
files:
database: 1 # 14-reports.sql (12 tablas)
backend: 14
frontend: 48 # Sprint 9 (24) + Sprint 10 (13) + Sprint 11 (11)
total: 63
backend: 17 # +3: pdf.service.ts, templates/report-templates.ts, export.service.ts updated
frontend: 56 # Sprint 9 (24) + Sprint 10 (13) + Sprint 11 (11) + Sprint 12 (4) + Sprint 13 (4)
tests: 4 # Sprint 13
total: 78
sprints:
- sprint: 8
@ -491,6 +595,16 @@ metrics:
status: completed
date: "2026-01-07"
feature: scheduled-reports
- sprint: 12
layer: frontend
status: completed
date: "2026-01-07"
feature: pages-navigation
- sprint: 13
layer: testing
status: completed
date: "2026-01-07"
feature: unit-tests
# =============================================================================
# HISTORIAL
@ -572,4 +686,40 @@ history:
- "4 formatos de exportacion: PDF, Excel, CSV, JSON"
- "TypeScript build validado"
- "Vite build validado"
- "Modulo MGN-009 completado al 100%"
- date: "2026-01-07"
action: "Sprint 12 - Pages & Navigation"
author: Frontend-Agent
changes:
- "ReportsPage - Landing page con cards de navegacion"
- "ReportBuilderPage - Integra componente ReportBuilder"
- "ScheduledReportsPage - Integra ScheduleList, ScheduleForm, ExecutionHistory"
- "routes.tsx - Rutas lazy loading para /reports/*"
- "DashboardLayout.tsx - Items Dashboards y Reportes en sidebar"
- "TypeScript build validado"
- "Vite build validado"
- date: "2026-01-07"
action: "Sprint 13 - Unit Tests"
author: Frontend-Agent
changes:
- "CronBuilder.test.tsx - 12 tests para constructor cron"
- "RecipientManager.test.tsx - 13 tests para gestion destinatarios"
- "FilterBuilder.test.tsx - 10 tests para constructor filtros"
- "EntityExplorer.test.tsx - 21 tests para explorador entidades"
- "Total: 56 tests nuevos, 94 tests en suite completa"
- "Cobertura: rendering, interaccion, validacion, estados"
- "Modulo MGN-009 completado al 100% con tests"
- date: "2026-01-07"
action: "BE-026 - PDF Export Implementation"
author: Backend-Agent
changes:
- "PdfService - Servicio puppeteer para generacion PDF"
- "ReportTemplates - Templates HTML para reportes financieros"
- "ExportService actualizado para usar PdfService"
- "Endpoints exportacion: /executions/:id/export, /trial-balance/export"
- "Endpoint health check: /pdf/health"
- "Soporte para formatos: PDF, XLSX, CSV, JSON, HTML"
- "Templates: tabular, financial, trialBalance, dashboard"
- "TypeScript build validado"

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View 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

View 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*

View File

@ -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

View 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*

View File

@ -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 |

View 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*

View 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*

File diff suppressed because it is too large Load Diff

View 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*

View 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*

View 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*

View 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*

View 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*

View File

@ -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 | - | - | [ ] |

View File

@ -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 | - | - | [ ] |

View File

@ -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 | - | - | [ ] |

View File

@ -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 | - | - | [ ] |

View File

@ -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 | - | - | [ ] |

View File

@ -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 | - | - | [ ] |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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