# Báo Cáo Doanh Thu Theo Khách Hàng - API CustomerRevenue

## Mô Tả

API `POST /api/RevenueReport/CustomerRevenue` trả về thống kê doanh thu theo khách hàng với phân loại theo trạng thái đơn hàng. Dữ liệu được tổng hợp từ stored procedure `sp_GetSaleRevenueByCustomer`.

## API Endpoint

**URL:** `POST /api/RevenueReport/CustomerRevenue`

**Authorization:** Requires `Report.CustomerRevenueStatistics` role

## Tham Số Đầu Vào

Request body sử dụng `SmartTableParam` với các tham số:

### Pagination (SmartTableParam.Pagination)

| Tham số | Kiểu dữ liệu | Bắt buộc | Mô tả | Mặc định |
|---------|--------------|----------|-------|----------|
| PageIndex | int | Không | Số trang (bắt đầu từ 1) | 1 |
| PageSize | int | Không | Số bản ghi mỗi trang | 25 |

### Search Filters (SmartTableParam.Search.PredicateObject)

| Tham số | Kiểu dữ liệu | Bắt buộc | Mô tả | Mặc định |
|---------|--------------|----------|-------|----------|
| StartDate | DateTimeOffset | Không | Ngày bắt đầu lọc | Đầu tháng hiện tại (00:00:00) |
| EndDate | DateTimeOffset | Không | Ngày kết thúc lọc | Thời điểm hiện tại |
| CustomerId | int | Không | ID khách hàng cụ thể | null |
| CustomerIds | int[] | Không | Danh sách ID khách hàng (chỉ Founder/Accountant) | null |
| SaleId | int | Không | ID sale để lọc khách hàng (chỉ Founder/Accountant) | null |

### Ví dụ Request

```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 25
  },
  "Search": {
    "PredicateObject": {
      "StartDate": "2026-01-01T00:00:00+07:00",
      "EndDate": "2026-01-31T23:59:59+07:00",
      "CustomerId": 123,
      "SaleId": 10
    }
  }
}
```

## Dữ Liệu Trả Về

Response trả về object `CustomerRevenueResult` kế thừa từ `BaseResult`:

```json
{
  "Result": 0,
  "Message": null,
  "Items": [
    {
      "CustomerId": 123,
      "CustomerCode": "KH001",
      "CustomerName": "Công ty ABC",
      "CustomerPhone": "0901234567",
      "ManagerId": 10,
      "ManagerName": "Nguyễn Văn A",
      "TotalOrders": 50,
      "TotalRevenue": 100000000.00,
      "TotalCollectAmount": 50000000.00,
      
      "WaitPickup": 5,
      "WaitPickupRevenue": 10000000.00,
      "WaitPickupCollectAmount": 5000000.00,
      
      "Picking": 3,
      "PickingRevenue": 6000000.00,
      "PickingCollectAmount": 3000000.00,
      
      "PickupFailed": 1,
      "PickupFailedRevenue": 2000000.00,
      "PickupFailedCollectAmount": 1000000.00,
      
      "Transporting": 2,
      "TransportingRevenue": 4000000.00,
      "TransportingCollectAmount": 2000000.00,
      
      "Delivering": 4,
      "DeliveringRevenue": 8000000.00,
      "DeliveringCollectAmount": 4000000.00,
      
      "DeliveryFailed": 1,
      "DeliveryFailedRevenue": 2000000.00,
      "DeliveryFailedCollectAmount": 1000000.00,
      
      "DeliverySuccess": 32,
      "DeliverySuccessRevenue": 64000000.00,
      "DeliverySuccessCollectAmount": 32000000.00,
      
      "Cancelled": 2,
      "CancelledRevenue": 4000000.00,
      "CancelledCollectAmount": 2000000.00,
      
      "TotalRecords": 50,
      "IsTotalRow": false,
      "TotalInProgress": 14,
      "TotalFailed": 4,
      "TotalCompleted": 32,
      "SuccessRate": 64.0,
      "TotalInProgressRevenue": 28000000.00,
      "TotalFailedRevenue": 8000000.00,
      "TotalCompletedRevenue": 64000000.00,
      "TotalInProgressCollectAmount": 14000000.00,
      "TotalFailedCollectAmount": 4000000.00,
      "TotalCompletedCollectAmount": 32000000.00
    },
    {
      "CustomerId": 0,
      "CustomerCode": "TỔNG CỘNG",
      "CustomerName": "TỔNG CỘNG",
      "CustomerPhone": "",
      "TotalOrders": 150,
      "TotalRevenue": 300000000.00,
      "TotalCollectAmount": 150000000.00,
      "TotalRecords": 50,
      "IsTotalRow": true,
      "SuccessRate": 68.0
    }
  ],
  "TotalRecord": 50
}
```

### CustomerRevenueResult

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| Result | int | Mã kết quả: 0 = Success, 2 = Failed, 3 = SystemError |
| Message | string | Thông báo lỗi (nếu có) |
| Items | List\<CustomerRevenueItem\> | Danh sách khách hàng |
| TotalRecord | double | Tổng số bản ghi |

### CustomerRevenueItem

#### Thông tin khách hàng

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| CustomerId | int | ID khách hàng (0 = dòng tổng cộng) |
| CustomerCode | string | Mã khách hàng |
| CustomerName | string | Tên khách hàng |
| CustomerPhone | string | Số điện thoại |
| ManagerId | int? | ID Sale quản lý khách hàng (null nếu không có) |
| ManagerName | string | Tên Sale quản lý khách hàng |

#### Tổng hợp

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| TotalOrders | int | Tổng số đơn hàng |
| TotalRevenue | decimal | Tổng doanh thu (phí vận chuyển) |
| TotalCollectAmount | decimal | Tổng tiền thu hộ (COD) |

#### Chờ lấy hàng (PhysicalStatus = 1)

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| WaitPickup | int | Số đơn |
| WaitPickupRevenue | decimal | Doanh thu |
| WaitPickupCollectAmount | decimal | Tiền thu hộ |

#### Đang lấy hàng (PhysicalStatus = 2)

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| Picking | int | Số đơn |
| PickingRevenue | decimal | Doanh thu |
| PickingCollectAmount | decimal | Tiền thu hộ |

#### Lấy hàng thất bại (PhysicalStatus = 3)

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| PickupFailed | int | Số đơn |
| PickupFailedRevenue | decimal | Doanh thu |
| PickupFailedCollectAmount | decimal | Tiền thu hộ |

#### Đang trung chuyển (PhysicalStatus = 4)

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| Transporting | int | Số đơn |
| TransportingRevenue | decimal | Doanh thu |
| TransportingCollectAmount | decimal | Tiền thu hộ |

#### Đang giao hàng (PhysicalStatus = 5)

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| Delivering | int | Số đơn |
| DeliveringRevenue | decimal | Doanh thu |
| DeliveringCollectAmount | decimal | Tiền thu hộ |

#### Giao hàng thất bại (PhysicalStatus = 6)

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| DeliveryFailed | int | Số đơn |
| DeliveryFailedRevenue | decimal | Doanh thu |
| DeliveryFailedCollectAmount | decimal | Tiền thu hộ |

#### Giao hàng thành công (PhysicalStatus = 7)

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| DeliverySuccess | int | Số đơn |
| DeliverySuccessRevenue | decimal | Doanh thu |
| DeliverySuccessCollectAmount | decimal | Tiền thu hộ |

#### Đã hủy (Status = 8)

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| Cancelled | int | Số đơn |
| CancelledRevenue | decimal | Doanh thu |
| CancelledCollectAmount | decimal | Tiền thu hộ |
| TotalRecords | int | Tổng số bản ghi (dùng cho phân trang) |

#### Computed Properties

| Field | Kiểu dữ liệu | Mô tả |
|-------|--------------|-------|
| IsTotalRow | bool | `true` nếu là dòng tổng cộng (CustomerId = 0) |
| TotalInProgress | int | Tổng đơn đang xử lý (WaitPickup + Picking + Transporting + Delivering) |
| TotalFailed | int | Tổng đơn thất bại (PickupFailed + DeliveryFailed + Cancelled) |
| TotalCompleted | int | Tổng đơn hoàn thành (DeliverySuccess) |
| SuccessRate | double | Tỷ lệ thành công (%) = (TotalCompleted / TotalOrders) * 100 |
| TotalInProgressRevenue | decimal | Tổng doanh thu đơn đang xử lý |
| TotalFailedRevenue | decimal | Tổng doanh thu đơn thất bại |
| TotalCompletedRevenue | decimal | Tổng doanh thu đơn hoàn thành |
| TotalInProgressCollectAmount | decimal | Tổng COD đơn đang xử lý |
| TotalFailedCollectAmount | decimal | Tổng COD đơn thất bại |
| TotalCompletedCollectAmount | decimal | Tổng COD đơn hoàn thành |

## Tính Toán Doanh Thu

Doanh thu (Revenue) được tính theo công thức:

```
Revenue = Fee + PickupSubFee + DeliverySubFee + CodFee + InsuranceFee + ReturnFee - Discount
```

Với:
- `Fee`: Cước giao hàng chính
- `PickupSubFee`: Phụ phí lấy hàng
- `DeliverySubFee`: Phụ phí giao hàng
- `CodFee`: Phí thu hộ (COD)
- `InsuranceFee`: Phí bảo hiểm
- `ReturnFee`: Phí hoàn trả
- `Discount`: Giảm giá

## Phân Quyền và Lọc Dữ Liệu

### Founder / Accountant
- Xem tất cả khách hàng
- Có thể lọc theo:
  - Khoảng thời gian (StartDate, EndDate)
  - Khách hàng cụ thể (CustomerId)
  - Danh sách khách hàng (CustomerIds)
  - Sale quản lý (SaleId)

### Sale
- Chỉ xem khách hàng có đơn hàng do mình phụ trách
- Có thể lọc theo:
  - Khoảng thời gian (StartDate, EndDate)
  - Khách hàng cụ thể (CustomerId - trong danh sách của mình)
- Tham số `CustomerIds` và `SaleId` sẽ bị bỏ qua

## Lưu Ý

1. **Phân trang**: 
   - Mặc định PageSize = 25, PageIndex = 1
   - Dữ liệu được sắp xếp theo TotalRevenue giảm dần (khách hàng có doanh số cao nhất ở đầu)
   - Dòng tổng cộng luôn xuất hiện ở cuối danh sách (nếu có trong trang hiện tại)
2. **Thời gian mặc định**: Nếu không truyền StartDate/EndDate, hệ thống tự động lấy từ đầu tháng hiện tại đến thời điểm hiện tại
3. **Dòng tổng cộng**: Response bao gồm dòng tổng với CustomerId = 0, được tính từ toàn bộ dữ liệu (không bị ảnh hưởng bởi phân trang)
4. **TotalRecords**: Giá trị này giống nhau cho tất cả các dòng, đại diện cho tổng số bản ghi (bao gồm cả dòng tổng cộng)
5. **Lọc theo IsDeleted**: Chỉ lấy đơn hàng chưa bị xóa (IsDeleted = 0)
6. **Lọc theo CreatedTime**: Dữ liệu được lọc theo thời gian tạo đơn hàng

## Cách Sử Dụng

### Ví dụ 1: Lấy tất cả khách hàng tháng hiện tại (Founder)

```bash
POST /api/RevenueReport/CustomerRevenue
Content-Type: application/json
Authorization: Bearer {token}

{
  "Search": {
    "PredicateObject": {}
  }
}
```

### Ví dụ 2: Lấy khách hàng cụ thể theo khoảng thời gian

```bash
POST /api/RevenueReport/CustomerRevenue
Content-Type: application/json
Authorization: Bearer {token}

{
  "Search": {
    "PredicateObject": {
      "StartDate": "2026-01-01T00:00:00+07:00",
      "EndDate": "2026-01-31T23:59:59+07:00",
      "CustomerId": 123
    }
  }
}
```

### Ví dụ 3: Lấy khách hàng theo sale (Founder/Accountant)

```bash
POST /api/RevenueReport/CustomerRevenue
Content-Type: application/json
Authorization: Bearer {token}

{
  "Search": {
    "PredicateObject": {
      "SaleId": 10
    }
  }
}
```

### Ví dụ 4: Lấy nhiều khách hàng (Founder/Accountant)

```bash
POST /api/RevenueReport/CustomerRevenue
Content-Type: application/json
Authorization: Bearer {token}

{
  "Search": {
    "PredicateObject": {
      "CustomerIds": [123, 456, 789]
    }
  }
}
```

## Xử Lý Lỗi

### Lỗi thường gặp

1. **Không xác định được người dùng**
   ```json
   {
     "Result": 2,
     "Message": "Không xác định được người dùng",
     "Items": [],
     "TotalRecord": 0
   }
   ```

2. **Không có quyền truy cập**
   - HTTP Status: 403 Forbidden
   - Response: Unauthorized

3. **Lỗi hệ thống**
   ```json
   {
     "Result": 3,
     "Message": "Lỗi hệ thống: {chi tiết lỗi}",
     "Items": [],
     "TotalRecord": 0
   }
   ```

## Tích Hợp

### Controller

```csharp
[HttpPost("CustomerRevenue")]
[Authorize(Roles = "Report.CustomerRevenueStatistics")]
public async Task<IActionResult> CustomerRevenue([FromBody] SmartTableParam param)
{
    var currentUser = User.GetCurrentUser();
    var result = await _saleReportService.GetCustomerRevenueStatisticsAsync(param, currentUser.UserId);
    return Ok(result);
}
```

### Service

```csharp
public async Task<CustomerRevenueResult> GetCustomerRevenueStatisticsAsync(SmartTableParam param, int userId)
{
    var result = new CustomerRevenueResult();
    const string storeName = "sp_GetSaleRevenueByCustomer";
    
    // Xử lý phân quyền và tham số
    // Gọi stored procedure
    // Map kết quả
    
    return result;
}
```

### Stored Procedure

**Name:** `sp_GetSaleRevenueByCustomer`

**Location:** `Scripts/sp_GetSaleRevenueByCustomer.sql`

**Parameters:**
- `@StartDate DATETIMEOFFSET` - Nullable, default: đầu tháng hiện tại
- `@EndDate DATETIMEOFFSET` - Nullable, default: thời điểm hiện tại
- `@CustomerId INT` - Nullable
- `@CustomerIds NVARCHAR(MAX)` - Nullable, comma-separated IDs
- `@SaleId INT` - Nullable
- `@PageNumber INT` - Default: 1
- `@PageSize INT` - Default: 25

## Performance

- Sử dụng stored procedure được tối ưu hóa
- Index đề xuất:
  - `Order.CreatedTime`
  - `Order.CustomerId`
  - `Order.SaleId`
  - `Order.IsDeleted`
  - `Customer.Id`
  - `OrderFin.Id`
  - `OrderOps.Id, Status, PhysicalStatus`

## Changelog

| Ngày | Phiên bản | Mô tả |
|------|-----------|-------|
| 2026-01-20 | 1.2 | Thêm thông tin sale quản lý (ManagerId, ManagerName) |
| 2026-01-20 | 1.1 | Thêm phân trang với PageNumber/PageSize, sắp xếp theo TotalRevenue DESC, thêm field TotalRecords |
| 2026-01-19 | 1.0 | Phiên bản đầu tiên - Báo cáo doanh thu theo khách hàng với phân loại theo trạng thái |

## Liên Hệ & Hỗ Trợ

Nếu có câu hỏi hoặc phát hiện lỗi, vui lòng liên hệ team phát triển.


**Description:** Lấy thống kê doanh thu theo khách hàng với phân loại theo trạng thái đơn hàng

**Authorization:** Requires `Report.CustomerRevenueStatistics` role

**Request Body:**
```json
{
  "search": {
    "predicateObject": {
      "StartDate": "2026-01-01T00:00:00+07:00",
      "EndDate": "2026-01-31T23:59:59+07:00",
      "CustomerId": 123,
      "CustomerIds": [123, 456, 789],
      "SaleId": 10
    }
  }
}
```

**Request Model:** `SmartTableParam`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| search.predicateObject.StartDate | DateTimeOffset | No | Ngày bắt đầu (mặc định: đầu tháng hiện tại) |
| search.predicateObject.EndDate | DateTimeOffset | No | Ngày kết thúc (mặc định: thời điểm hiện tại) |
| search.predicateObject.CustomerId | int | No | ID khách hàng cụ thể |
| search.predicateObject.CustomerIds | int[] | No | Danh sách ID khách hàng (chỉ Founder/Accountant) |
| search.predicateObject.SaleId | int | No | ID sale để lọc khách hàng (chỉ Founder/Accountant) |

**Response:** `CustomerRevenueResult`
```json
{
  "result": 0,
  "message": null,
  "items": [
    {
      "customerId": 123,
      "customerCode": "KH001",
      "customerName": "Công ty ABC",
      "customerPhone": "0901234567",
      "managerId": 10,
      "managerName": "Nguyễn Văn A",
      "totalOrders": 50,
      "totalRevenue": 100000000,
      "totalCollectAmount": 50000000,
      
      "waitPickup": 5,
      "waitPickupRevenue": 10000000,
      "waitPickupCollectAmount": 5000000,
      
      "picking": 3,
      "pickingRevenue": 6000000,
      "pickingCollectAmount": 3000000,
      
      "pickupFailed": 1,
      "pickupFailedRevenue": 2000000,
      "pickupFailedCollectAmount": 1000000,
      
      "transporting": 2,
      "transportingRevenue": 4000000,
      "transportingCollectAmount": 2000000,
      
      "delivering": 4,
      "deliveringRevenue": 8000000,
      "deliveringCollectAmount": 4000000,
      
      "deliveryFailed": 1,
      "deliveryFailedRevenue": 2000000,
      "deliveryFailedCollectAmount": 1000000,
      
      "deliverySuccess": 32,
      "deliverySuccessRevenue": 64000000,
      "deliverySuccessCollectAmount": 32000000,
      
      "cancelled": 2,
      "cancelledRevenue": 4000000,
      "cancelledCollectAmount": 2000000,
      
      "isTotalRow": false,
      "totalInProgress": 14,
      "totalFailed": 4,
      "totalCompleted": 32,
      "successRate": 64.0,
      "totalInProgressRevenue": 28000000,
      "totalFailedRevenue": 8000000,
      "totalCompletedRevenue": 64000000,
      "totalInProgressCollectAmount": 14000000,
      "totalFailedCollectAmount": 4000000,
      "totalCompletedCollectAmount": 32000000
    },
    {
      "customerId": 0,
      "customerCode": "TỔNG CỘNG",
      "customerName": "TỔNG CỘNG",
      "customerPhone": "",
      "totalOrders": 150,
      "totalRevenue": 300000000,
      "totalCollectAmount": 150000000,
      "isTotalRow": true,
      "successRate": 68.0
    }
  ],
  "totalRecord": 2
}
```

**Status Codes:**
- `200 OK`: Request successful
- `400 Bad Request`: Invalid parameters or validation errors
- `401 Unauthorized`: User not authenticated
- `403 Forbidden`: User doesn't have required permissions
- `500 Internal Server Error`: System error

---

## Response Data Structure

### CustomerRevenueItem

| Field | Type | Description |
|-------|------|-------------|
| customerId | int | ID khách hàng (0 = dòng tổng cộng) |
| customerCode | string | Mã khách hàng |
| customerName | string | Tên khách hàng |
| customerPhone | string | Số điện thoại khách hàng |
| managerId | int? | ID Sale quản lý khách hàng (null nếu không có) |
| managerName | string | Tên Sale quản lý khách hàng |
| totalOrders | int | Tổng số đơn hàng |
| totalRevenue | decimal | Tổng doanh thu (phí vận chuyển) |
| totalCollectAmount | decimal | Tổng tiền thu hộ (COD) |

#### Order Status Groups

**Chờ lấy hàng (PhysicalStatus = 1):**
- `waitPickup` (int): Số đơn
- `waitPickupRevenue` (decimal): Doanh thu
- `waitPickupCollectAmount` (decimal): Tiền thu hộ

**Đang lấy hàng (PhysicalStatus = 2):**
- `picking` (int): Số đơn
- `pickingRevenue` (decimal): Doanh thu
- `pickingCollectAmount` (decimal): Tiền thu hộ

**Lấy hàng thất bại (PhysicalStatus = 3):**
- `pickupFailed` (int): Số đơn
- `pickupFailedRevenue` (decimal): Doanh thu
- `pickupFailedCollectAmount` (decimal): Tiền thu hộ

**Đang trung chuyển (PhysicalStatus = 4):**
- `transporting` (int): Số đơn
- `transportingRevenue` (decimal): Doanh thu
- `transportingCollectAmount` (decimal): Tiền thu hộ

**Đang giao hàng (PhysicalStatus = 5):**
- `delivering` (int): Số đơn
- `deliveringRevenue` (decimal): Doanh thu
- `deliveringCollectAmount` (decimal): Tiền thu hộ

**Giao hàng thất bại (PhysicalStatus = 6):**
- `deliveryFailed` (int): Số đơn
- `deliveryFailedRevenue` (decimal): Doanh thu
- `deliveryFailedCollectAmount` (decimal): Tiền thu hộ

**Giao hàng thành công (PhysicalStatus = 7):**
- `deliverySuccess` (int): Số đơn
- `deliverySuccessRevenue` (decimal): Doanh thu
- `deliverySuccessCollectAmount` (decimal): Tiền thu hộ

**Đã hủy (Status = 8):**
- `cancelled` (int): Số đơn
- `cancelledRevenue` (decimal): Doanh thu
- `cancelledCollectAmount` (decimal): Tiền thu hộ

#### Computed Properties

| Field | Type | Description |
|-------|------|-------------|
| isTotalRow | bool | `true` nếu là dòng tổng cộng (customerId = 0) |
| totalInProgress | int | Tổng đơn đang xử lý (waitPickup + picking + transporting + delivering) |
| totalFailed | int | Tổng đơn thất bại (pickupFailed + deliveryFailed + cancelled) |
| totalCompleted | int | Tổng đơn hoàn thành (deliverySuccess) |
| successRate | double | Tỷ lệ thành công (%) = (totalCompleted / totalOrders) * 100 |
| totalInProgressRevenue | decimal | Tổng doanh thu đơn đang xử lý |
| totalFailedRevenue | decimal | Tổng doanh thu đơn thất bại |
| totalCompletedRevenue | decimal | Tổng doanh thu đơn hoàn thành |
| totalInProgressCollectAmount | decimal | Tổng COD đơn đang xử lý |
| totalFailedCollectAmount | decimal | Tổng COD đơn thất bại |
| totalCompletedCollectAmount | decimal | Tổng COD đơn hoàn thành |

---

## Business Logic

### Default Date Range
Nếu không truyền `StartDate` và `EndDate`, hệ thống sẽ tự động set:
- **StartDate**: Đầu tháng hiện tại (00:00:00)
- **EndDate**: Thời điểm hiện tại

Logic này được xử lý trong stored procedure `sp_GetSaleRevenueByCustomer`.

### Permission & Data Access

**Founder / Accountant:**
- Xem tất cả khách hàng
- Có thể lọc theo:
  - Khoảng thời gian
  - Khách hàng cụ thể (`CustomerId`)
  - Danh sách khách hàng (`CustomerIds`)
  - Sale quản lý (`SaleId`)

**Sale:**
- Chỉ xem khách hàng có đơn hàng do mình phụ trách
- Có thể lọc theo:
  - Khoảng thời gian
  - Khách hàng cụ thể (trong danh sách của mình)
- Tham số `CustomerIds` và `SaleId` sẽ bị bỏ qua

### Revenue Calculation
Doanh thu (Revenue) được tính bằng công thức:
```
Revenue = Fee + PickupSubFee + DeliverySubFee + CodFee + InsuranceFee + ReturnFee - Discount
```

### Total Row
Response luôn bao gồm một dòng tổng cộng với:
- `customerId = 0`
- `customerCode = "TỔNG CỘNG"`
- `customerName = "TỔNG CỘNG"`
- Các giá trị khác là tổng của tất cả khách hàng

---

## Usage Examples

### Example 1: Get all customers for current month (Founder)
```bash
POST /api/RevenueReport/CustomerRevenue
Content-Type: application/json
Authorization: Bearer {token}

{
  "search": {
    "predicateObject": {}
  }
}
```
Sẽ trả về dữ liệu từ đầu tháng hiện tại đến thời điểm hiện tại.

### Example 2: Get specific customer for date range
```bash
POST /api/RevenueReport/CustomerRevenue
Content-Type: application/json
Authorization: Bearer {token}

{
  "search": {
    "predicateObject": {
      "StartDate": "2026-01-01T00:00:00+07:00",
      "EndDate": "2026-01-31T23:59:59+07:00",
      "CustomerId": 123
    }
  }
}
```

### Example 3: Get customers by sale (Founder/Accountant only)
```bash
POST /api/RevenueReport/CustomerRevenue
Content-Type: application/json
Authorization: Bearer {token}

{
  "search": {
    "predicateObject": {
      "StartDate": "2026-01-01T00:00:00+07:00",
      "EndDate": "2026-01-31T23:59:59+07:00",
      "SaleId": 10
    }
  }
}
```
Sẽ trả về tất cả khách hàng có đơn hàng do sale ID 10 phụ trách.

### Example 4: Get multiple customers (Founder/Accountant only)
```bash
POST /api/RevenueReport/CustomerRevenue
Content-Type: application/json
Authorization: Bearer {token}

{
  "search": {
    "predicateObject": {
      "CustomerIds": [123, 456, 789]
    }
  }
}
```

---

## Error Handling

### Common Error Scenarios

1. **Invalid User**
   ```json
   {
     "result": 2,
     "message": "Không xác định được người dùng"
   }
   ```

2. **No Permission**
   ```json
   {
     "result": 2,
     "message": "Bạn không có quyền truy cập báo cáo này"
   }
   ```
   HTTP Status: 403 Forbidden

3. **System Error**
   ```json
   {
     "result": 3,
     "message": "Lỗi hệ thống: {error details}"
   }
   ```
   HTTP Status: 500 Internal Server Error

---

## Security Considerations

1. **Authorization**: Chỉ users có role `Report.CustomerRevenueStatistics` mới có thể truy cập
2. **Data Isolation**: 
   - Sale chỉ xem được khách hàng của mình
   - Founder/Accountant xem được tất cả
3. **Parameter Validation**: 
   - `CustomerIds` và `SaleId` chỉ có hiệu lực với Founder/Accountant
   - Sale users không thể bypass bằng cách truyền các tham số này

---

## Performance Considerations

1. **Database Query**: Sử dụng stored procedure `sp_GetSaleRevenueByCustomer` được tối ưu hóa
2. **Indexing**: Đảm bảo các indexes trên:
   - `Order.CreatedTime`
   - `Order.CustomerId`
   - `Order.SaleId`
   - `Order.IsDeleted`
3. **Large Datasets**: 
   - Với khoảng thời gian dài, query có thể mất vài giây
   - Khuyến nghị giới hạn khoảng thời gian tối đa 1 năm
4. **Async Processing**: Toàn bộ pipeline sử dụng async/await

---

## Related APIs
- [Sale Revenue Statistics API](./API_SaleRevenueStatistics.md)
- [Order Revenue by Date API](./API_OrderRevenueByDate.md)
- [Order Revenue by Sale API](./API_OrderRevenueBySale.md)

---

## Implementation Notes

### Service Dependencies
- `ISaleReportService`: Service chính xử lý logic báo cáo
- `IUserService`: Kiểm tra user và roles
- `IDbConnection`: Kết nối database để gọi stored procedure

### Stored Procedure
**Name:** `sp_GetSaleRevenueByCustomer`

**Location:** `Scripts/sp_GetSaleRevenueByCustomer.sql`

**Parameters:**
- `@StartDate DATETIMEOFFSET` - Nullable, default: đầu tháng hiện tại
- `@EndDate DATETIMEOFFSET` - Nullable, default: thời điểm hiện tại
- `@CustomerId INT` - Nullable
- `@CustomerIds NVARCHAR(MAX)` - Nullable, comma-separated IDs
- `@SaleId INT` - Nullable

**Returns:** Danh sách `CustomerRevenueItem` bao gồm dòng tổng cộng

### Data Flow
1. Controller nhận request và extract `currentUser`
2. Service kiểm tra roles và xác định quyền truy cập
3. Parse tham số từ `SmartTableParam`
4. Gọi stored procedure với Dapper
5. Map kết quả sang `CustomerRevenueItem`
6. Trả về `CustomerRevenueResult`

---

## Version History
- **v1.0** (2026-01-19): Initial release
  - Basic customer revenue statistics
  - Support filtering by date range, customer, and sale
  - Role-based access control
