---
id: "ET-PFM-006"
title: "Motor de Reportes Fiscales"
type: "Technical Specification"
status: "Done"
priority: "Alta"
epic: "OQI-008"
project: "trading-platform"
version: "1.0.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---

# ET-PFM-006: Motor de Reportes Fiscales

**Épica:** OQI-008 - Portfolio Manager
**Versión:** 1.0
**Fecha:** 2025-12-05
**Estado:** Planificado

---

## Servicios de Cálculo Fiscal

```typescript
@Injectable()
export class TaxReportService {

  async generateCapitalGainsReport(
    userId: string,
    year: number
  ): Promise<CapitalGainsReport> {
    const sales = await this.getSalesForYear(userId, year);

    const gains: CapitalGain[] = [];
    let shortTermTotal = 0;
    let longTermTotal = 0;

    for (const sale of sales) {
      const costBasis = await this.getCostBasis(sale, 'FIFO');
      const gain = sale.total - costBasis.totalCost;
      const holdingDays = this.calculateHoldingPeriod(
        costBasis.purchaseDate,
        sale.executedAt
      );
      const isLongTerm = holdingDays > 365;

      gains.push({
        symbol: sale.symbol,
        purchaseDate: costBasis.purchaseDate,
        saleDate: sale.executedAt,
        quantity: sale.quantity,
        costBasis: costBasis.totalCost,
        proceeds: sale.total,
        gain,
        holdingPeriod: isLongTerm ? 'long' : 'short',
      });

      if (isLongTerm) {
        longTermTotal += gain;
      } else {
        shortTermTotal += gain;
      }
    }

    return {
      year,
      gains,
      summary: {
        shortTerm: {
          gains: gains.filter(g => g.holdingPeriod === 'short' && g.gain > 0)
            .reduce((sum, g) => sum + g.gain, 0),
          losses: gains.filter(g => g.holdingPeriod === 'short' && g.gain < 0)
            .reduce((sum, g) => sum + g.gain, 0),
          net: shortTermTotal,
        },
        longTerm: {
          gains: gains.filter(g => g.holdingPeriod === 'long' && g.gain > 0)
            .reduce((sum, g) => sum + g.gain, 0),
          losses: gains.filter(g => g.holdingPeriod === 'long' && g.gain < 0)
            .reduce((sum, g) => sum + g.gain, 0),
          net: longTermTotal,
        },
        total: shortTermTotal + longTermTotal,
      },
    };
  }

  async generateDividendReport(
    userId: string,
    year: number
  ): Promise<DividendReport> {
    const dividends = await this.getDividendsForYear(userId, year);

    return {
      year,
      dividends: dividends.map(d => ({
        date: d.executedAt,
        symbol: d.symbol,
        amount: d.total,
        type: d.metadata?.qualified ? 'qualified' : 'ordinary',
      })),
      summary: {
        qualified: dividends
          .filter(d => d.metadata?.qualified)
          .reduce((sum, d) => sum + d.total, 0),
        ordinary: dividends
          .filter(d => !d.metadata?.qualified)
          .reduce((sum, d) => sum + d.total, 0),
        total: dividends.reduce((sum, d) => sum + d.total, 0),
      },
    };
  }
}
```

---

## Tax-Loss Harvesting (Premium)

```typescript
async getTaxLossOpportunities(
  userId: string
): Promise<TaxLossOpportunity[]> {
  const positions = await this.portfolioService.getPositions(userId);
  const ytdGains = await this.getYTDRealizedGains(userId);

  const opportunities = positions
    .filter(p => p.pnl < 0)
    .map(p => ({
      symbol: p.symbol,
      unrealizedLoss: p.pnl,
      estimatedTaxSavings: Math.abs(p.pnl) * 0.25, // ~25% tax rate
      washSaleStatus: this.checkWashSale(p.symbol, userId),
      canHarvest: this.checkWashSale(p.symbol, userId) === 'clear',
    }))
    .filter(o => o.canHarvest);

  return opportunities;
}
```

---

## Referencias

- [RF-PFM-006: Reportes Fiscales](../requerimientos/RF-PFM-006-reportes-fiscales.md)

---

*Especificación técnica - Sistema NEXUS*
