<!-- 30fafa34-4e35-4e8c-b4c1-16f955707e2d 9dfc7776-9756-4da2-bca8-cf91a327ee96 -->
# Kế hoạch xây dựng API báo cáo Thu Chi - Dựa trên Transaction

## Tổng quan

Xây dựng API trả về dữ liệu thu chi theo khoảng thời gian (FromDate - ToDate), với cấu trúc:

- **Nguồn dữ liệu chính**: Bảng **Transaction** (tập trung tất cả giao dịch thu chi)
- **Phân loại theo**: `TransactionDirection` (Income/Outcome) và `TransactionType`
- **Chi tiết chi phí**: Dựa vào `CostCategory` (liên kết với `ExpenseCategory`)
- **Cột đầu tiên**: Danh mục thu/chi
- **Các cột tiếp theo**: Mỗi ngày một cột (tổng hợp theo ngày)

## Phần A: Sửa bug CostCategory (Priority cao)

### Vấn đề hiện tại

Trong `ExpenseService.MarkAsPaid()` (dòng 667-688), khi tạo Transaction từ Expense:

- `TransactionType = Withdraw`
- **THIẾU**: `CostCategoryId` không được set từ `expense.ExpenseCategoryId`

### Giải pháp

**File**: `Ladaer.BackEnd/Infrastructures/Services/Finances/ExpenseService.cs`

Sửa dòng 668-688, thêm:

```csharp
var transaction = new Transaction
{
    Id = Guid.NewGuid(),
    Description = $"Chi phí: {expense.Description}",
    Note = string.IsNullOrWhiteSpace(note) ? $"Chuyển tiền cho chi phí {expense.ExpenseCategory?.Name}" : note,
    Amount = expense.Amount,
    CashAmount = 0,
    TransferAmount = expense.Amount,
    WalletId = wallet.Id,
    TransactionType = TransactionType.Withdraw,
    TransactionDirection = TransactionDirection.Outcome,
    TransactionDate = DateTimeOffset.Now,
    FromUserId = currentUser.UserId,
    CostCategoryId = expense.ExpenseCategoryId, // <-- THÊM DÒNG NÀY
    // ... các field khác
};
```

## Phần B: Xây dựng API báo cáo

### Bước 1: Tạo DTO models

**File**: `Ladaer.BackEnd/Infrastructures/DTO/Models/Finance/IncomeExpenseReportModel.cs`

```csharp
public class IncomeExpenseReportRequest
{
    public DateTimeOffset FromDate { get; set; }
    public DateTimeOffset ToDate { get; set; }
}
```

**File**: `Ladaer.BackEnd/Infrastructures/DTO/Results/Finance/IncomeExpenseReportResult.cs`

```csharp
public class IncomeExpenseReportRowItem
{
    public string CategoryName { get; set; }
    public int? CategoryId { get; set; }
    public int? ParentId { get; set; }
    public int Level { get; set; }
    public string RowType { get; set; } // "Income", "Expense", "Total"
    public Dictionary<string, decimal> DailyAmounts { get; set; }
    public decimal Total { get; set; }
}

public class IncomeExpenseReportResult : BaseResult
{
    public DateTimeOffset FromDate { get; set; }
    public DateTimeOffset ToDate { get; set; }
    public List<string> DateColumns { get; set; }
    public List<IncomeExpenseReportRowItem> Rows { get; set; }
    public decimal TotalIncome { get; set; }
    public decimal TotalExpense { get; set; }
    public decimal NetAmount { get; set; }
}

// Model nhận từ stored procedure
public class TransactionReportRow
{
    public DateTime ReportDate { get; set; }
    public string TransactionType { get; set; }
    public string TransactionDirection { get; set; } // "Income" or "Outcome"
    public int? CostCategoryId { get; set; }
    public string CategoryName { get; set; }
    public int? ParentId { get; set; }
    public int Level { get; set; }
    public int DisplayOrder { get; set; }
    public decimal Amount { get; set; }
    public int TransactionCount { get; set; }
}
```

### Bước 2: Tạo Stored Procedure

**File**: `Ladaer.BackEnd/Scripts/sp_GetIncomeExpenseReport.sql`

Logic tổng hợp:

- **Thu (Income)**: 
  - `OrderFee` -> "Thu cước phí"
  - `CollectFromDriver` -> "Thu tiền từ lái xe"
  - `CollectFromCustomer` -> "Thu tiền từ khách hàng"
  - `Recharge` -> "Nạp tiền"

- **Chi (Outcome)**:
  - `Reconciliation` -> "Chi phí đối soát"
  - `Withdraw` -> Chi phí vận hành (theo ExpenseCategory từ CostCategoryId)
```sql
CREATE PROCEDURE sp_GetIncomeExpenseReport
    @FromDate DATETIMEOFFSET,
    @ToDate DATETIMEOFFSET
AS
BEGIN
    SET NOCOUNT ON;
    
    -- CTE để lấy thông tin ExpenseCategory hierarchy
    ;WITH CategoryHierarchy AS (
        SELECT 
            Id,
            Name,
            ParentId,
            DisplayOrder,
            0 AS Level
        FROM ExpenseCategories
        WHERE ParentId IS NULL AND IsDeleted = 0
        
        UNION ALL
        
        SELECT 
            ec.Id,
            ec.Name,
            ec.ParentId,
            ec.DisplayOrder,
            ch.Level + 1
        FROM ExpenseCategories ec
        INNER JOIN CategoryHierarchy ch ON ec.ParentId = ch.Id
        WHERE ec.IsDeleted = 0
    )
    
    -- Lấy dữ liệu Transaction
    SELECT 
        CAST(t.TransactionDate AS DATE) AS ReportDate,
        CASE t.TransactionType
            WHEN 3 THEN N'Thu cước phí' -- OrderFee
            WHEN 4 THEN N'Thu tiền từ lái xe' -- CollectFromDriver  
            WHEN 6 THEN N'Thu tiền từ khách hàng' -- CollectFromCustomer
            WHEN 1 THEN N'Nạp tiền' -- Recharge
            WHEN 5 THEN N'Chi phí đối soát' -- Reconciliation
            WHEN 2 THEN ISNULL(ch.Name, N'Chi phí khác') -- Withdraw (với category)
            ELSE N'Khác'
        END AS CategoryName,
        CASE t.TransactionDirection
            WHEN 1 THEN 'Income'
            WHEN 2 THEN 'Outcome'
        END AS TransactionDirection,
        t.TransactionType,
        t.CostCategoryId,
        ch.ParentId,
        ISNULL(ch.Level, 0) AS Level,
        ISNULL(ch.DisplayOrder, 999) AS DisplayOrder,
        SUM(t.Amount) AS Amount,
        COUNT(t.Id) AS TransactionCount
    FROM Transactions t
    LEFT JOIN CategoryHierarchy ch ON t.CostCategoryId = ch.Id
    WHERE t.TransactionDate >= @FromDate 
        AND t.TransactionDate <= @ToDate
        AND t.IsDeleted = 0
    GROUP BY 
        CAST(t.TransactionDate AS DATE),
        t.TransactionType,
        t.TransactionDirection,
        t.CostCategoryId,
        ch.Name,
        ch.ParentId,
        ch.Level,
        ch.DisplayOrder
    ORDER BY 
        t.TransactionDirection, -- Income trước, Outcome sau
        CASE 
            WHEN t.TransactionDirection = 2 AND t.TransactionType = 2 THEN ch.Level -- Withdraw thì sắp theo level
            ELSE 0
        END,
        CASE 
            WHEN t.TransactionDirection = 2 AND t.TransactionType = 2 THEN ch.DisplayOrder
            ELSE t.TransactionType
        END,
        ReportDate;
END
```


### Bước 3: Tạo Service

**File**: `Ladaer.BackEnd/Infrastructures/Services/Report/Finance/IncomeExpenseReportService.cs`

```csharp
public interface IIncomeExpenseReportService
{
    Task<IncomeExpenseReportResult> GetIncomeExpenseReportAsync(
        IncomeExpenseReportRequest request, 
        int userId);
}

public class IncomeExpenseReportService : IIncomeExpenseReportService
{
    private readonly IDbConnection _dbConnection;
    
    public async Task<IncomeExpenseReportResult> GetIncomeExpenseReportAsync(
        IncomeExpenseReportRequest request, int userId)
    {
        var result = new IncomeExpenseReportResult
        {
            FromDate = request.FromDate,
            ToDate = request.ToDate,
            Result = Result.Success
        };
        
        try
        {
            // Validate
            if (request.FromDate > request.ToDate)
            {
                result.Result = Result.Failed;
                result.Message = "Ngày bắt đầu phải nhỏ hơn ngày kết thúc";
                return result;
            }
            
            var parameters = new DynamicParameters();
            parameters.Add("FromDate", request.FromDate);
            parameters.Add("ToDate", request.ToDate);
            
            // Gọi stored procedure
            var transactionData = (await _dbConnection.QueryAsync<TransactionReportRow>(
                "sp_GetIncomeExpenseReport",
                parameters,
                commandType: CommandType.StoredProcedure
            )).ToList();
            
            // Generate date columns
            result.DateColumns = GenerateDateColumns(request.FromDate, request.ToDate);
            
            // Build report matrix
            result.Rows = BuildReportMatrix(transactionData, result.DateColumns);
            
            // Calculate totals
            CalculateTotals(result);
        }
        catch (Exception ex)
        {
            result.Result = Result.SystemError;
            result.Message = $"Lỗi khi tạo báo cáo: {ex.Message}";
        }
        
        return result;
    }
    
    private List<string> GenerateDateColumns(DateTimeOffset from, DateTimeOffset to)
    {
        var dates = new List<string>();
        var current = from.Date;
        var end = to.Date;
        
        while (current <= end)
        {
            dates.Add(current.ToString("yyyy-MM-dd"));
            current = current.AddDays(1);
        }
        
        return dates;
    }
    
    private List<IncomeExpenseReportRowItem> BuildReportMatrix(
        List<TransactionReportRow> data,
        List<string> dateColumns)
    {
        var rows = new List<IncomeExpenseReportRowItem>();
        
        // Nhóm theo Income và Outcome
        var incomeData = data.Where(d => d.TransactionDirection == "Income").ToList();
        var expenseData = data.Where(d => d.TransactionDirection == "Outcome").ToList();
        
        // Xây dựng phần THU
        if (incomeData.Any())
        {
            rows.AddRange(BuildIncomeRows(incomeData, dateColumns));
        }
        
        // Xây dựng phần CHI
        if (expenseData.Any())
        {
            rows.AddRange(BuildExpenseRows(expenseData, dateColumns));
        }
        
        return rows;
    }
    
    private List<IncomeExpenseReportRowItem> BuildIncomeRows(
        List<TransactionReportRow> incomeData,
        List<string> dateColumns)
    {
        // Group theo CategoryName
        var grouped = incomeData
            .GroupBy(d => new { d.CategoryName, d.Level })
            .OrderBy(g => g.Key.CategoryName);
        
        var rows = new List<IncomeExpenseReportRowItem>();
        
        foreach (var group in grouped)
        {
            var row = new IncomeExpenseReportRowItem
            {
                CategoryName = group.Key.CategoryName,
                Level = group.Key.Level,
                RowType = "Income",
                DailyAmounts = new Dictionary<string, decimal>()
            };
            
            // Điền số liệu theo ngày
            foreach (var dateCol in dateColumns)
            {
                var dateValue = DateTime.Parse(dateCol);
                var amount = group
                    .Where(d => d.ReportDate.Date == dateValue)
                    .Sum(d => d.Amount);
                row.DailyAmounts[dateCol] = amount;
            }
            
            row.Total = row.DailyAmounts.Values.Sum();
            rows.Add(row);
        }
        
        // Thêm dòng tổng thu
        rows.Add(CreateTotalRow("Tổng thu", rows, dateColumns, "Income"));
        
        return rows;
    }
    
    private List<IncomeExpenseReportRowItem> BuildExpenseRows(
        List<TransactionReportRow> expenseData,
        List<string> dateColumns)
    {
        var rows = new List<IncomeExpenseReportRowItem>();
        
        // Tách ra: Đối soát và Chi phí vận hành
        var reconciliationData = expenseData.Where(d => d.CategoryName == "Chi phí đối soát").ToList();
        var operatingExpenseData = expenseData.Where(d => d.CategoryName != "Chi phí đối soát").ToList();
        
        // Xử lý đối soát
        if (reconciliationData.Any())
        {
            rows.Add(BuildSingleCategoryRow("Chi phí đối soát", reconciliationData, dateColumns, 0));
        }
        
        // Xử lý chi phí vận hành (có cấu trúc cây)
        if (operatingExpenseData.Any())
        {
            rows.AddRange(BuildExpenseTreeRows(operatingExpenseData, dateColumns));
        }
        
        // Thêm dòng tổng chi
        rows.Add(CreateTotalRow("Tổng chi", rows, dateColumns, "Expense"));
        
        return rows;
    }
}
```

### Bước 4: Tạo Controller

**File**: `Ladaer.BackEnd/Controllers/Finance/IncomeExpenseReportController.cs`

```csharp
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class IncomeExpenseReportController : ControllerBase
{
    private readonly IIncomeExpenseReportService _service;

    public IncomeExpenseReportController(IIncomeExpenseReportService service)
    {
        _service = service;
    }

    /// <summary>
    /// Lấy báo cáo thu chi theo khoảng thời gian
    /// </summary>
    [HttpPost("GetReport")]
    [Authorize(Roles = "IncomeExpenseReport.View")]
    public async Task<IActionResult> GetReport([FromBody] IncomeExpenseReportRequest request)
    {
        var currentUser = User.GetCurrentUser();
        var result = await _service.GetIncomeExpenseReportAsync(request, currentUser.UserId);
        return Ok(result);
    }
}
```

### Bước 5: Đăng ký Service

**File**: `Ladaer.BackEnd/Infrastructures/Startup/DependencyRegister.cs`

Thêm:

```csharp
services.AddScoped<IIncomeExpenseReportService, IncomeExpenseReportService>();
```

## Ưu điểm của approach này

1. **Tập trung dữ liệu**: Transaction là nguồn chân lý duy nhất cho tất cả giao dịch
2. **Dễ mở rộng**: Thêm TransactionType mới chỉ cần update SP
3. **Performance**: Stored procedure tối ưu với CTE hierarchy
4. **Nhất quán**: CostCategory được sync từ ExpenseCategory
5. **Audit trail**: Transaction lưu đầy đủ thông tin lịch sử

## Files cần tạo/sửa

1. `Ladaer.BackEnd/Infrastructures/Services/Finances/ExpenseService.cs` (sửa - thêm CostCategoryId)
2. `Ladaer.BackEnd/Scripts/sp_GetIncomeExpenseReport.sql` (mới)
3. `Ladaer.BackEnd/Infrastructures/DTO/Models/Finance/IncomeExpenseReportModel.cs` (mới)
4. `Ladaer.BackEnd/Infrastructures/DTO/Results/Finance/IncomeExpenseReportResult.cs` (mới)
5. `Ladaer.BackEnd/Infrastructures/Services/Report/Finance/IncomeExpenseReportService.cs` (mới)
6. `Ladaer.BackEnd/Controllers/Finance/IncomeExpenseReportController.cs` (mới)
7. `Ladaer.BackEnd/Infrastructures/Startup/DependencyRegister.cs` (sửa)

### To-dos

- [ ] Tạo DTO models (Request và Response) cho báo cáo thu chi
- [ ] Tạo interface IIncomeExpenseReportService
- [ ] Implement logic tính toán phần THU (COD và Cước phí)
- [ ] Implement logic tính toán chi phí đối soát COD
- [ ] Implement logic chi phí vận hành theo cây ExpenseCategory
- [ ] Tổng hợp dữ liệu thu chi theo cấu trúc ma trận (danh mục x ngày)
- [ ] Tạo IncomeExpenseReportController với endpoint GetReport
- [ ] Đăng ký service trong DependencyRegister
- [ ] Test API với dữ liệu thực tế và xử lý edge cases