# API Documentation - Quản lý Thưởng / Phạt (Rewards & Penalties)

## Mục lục
1. [Phiếu Thưởng/Phạt (RewardPenalty)](#1-phiếu-thưởngphạt-rewardpenalty)
2. [Cấu hình Phạt Tự động (AutoPenaltyConfig)](#2-cấu-hình-phạt-tự-động-autopenaltyconfig)
3. [Phụ lục Enum](#3-phụ-lục-enum)

---

## 1. Phiếu Thưởng/Phạt (RewardPenalty)

### 1.1. Tìm kiếm phiếu
- **Endpoint**: `POST /api/RewardPenalty/Search`
- **Authorization**: `RewardPenalty.Search`

**Request Body (SmartTableParam):**
```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 15
  },
  "Search": {
    "PredicateObject": {
      "Keyword": "RP-2026"
    }
  },
  "Sort": {
    "Predicate": "CreatedTime",
    "Reverse": true
  }
}
```

**Request Parameters:**
- `Keyword` (string): Tìm kiếm theo mã phiếu (`Code`)

> [!NOTE]
> Hệ thống luôn sắp xếp theo `CreatedTime` giảm dần (mới nhất trước), bất kể giá trị `Sort` gửi lên.

**Response:**
```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 15,
    "TotalItemCount": 100,
    "TotalPages": 7
  },
  "Items": [
    {
      "Id": 1,
      "Code": "RP-20260312-A1B2C3",
      "UserId": 15,
      "FullName": "Nguyễn Văn A",
      "UserName": "nguyenvana",
      "TicketType": 20,
      "Source": 10,
      "Amount": 500000,
      "Reason": "Vi phạm nội quy",
      "Status": 20,
      "ViolationId": null,
      "BusinessDate": "2026-03-11T00:00:00",
      "ForgivenBy": null,
      "ForgivenAt": null,
      "ForgiveReason": null
    }
  ]
}
```

**Response Fields:**
| Trường | Kiểu | Mô tả |
|---|---|---|
| `Id` | int | ID phiếu |
| `Code` | string | Mã phiếu (format: `RP-yyyyMMdd-XXXXXX`) |
| `UserId` | int | ID nhân viên bị thưởng/phạt |
| `FullName` | string | Họ tên nhân viên |
| `UserName` | string | Tên đăng nhập nhân viên |
| `TicketType` | int | Loại phiếu (xem [Enum TicketType](#31-tickettype-loại-phiếu)) |
| `Source` | int | Nguồn gốc (xem [Enum TicketSource](#32-ticketsource-nguồn-gốc)) |
| `Amount` | decimal | Số tiền thưởng/phạt |
| `Reason` | string | Lý do |
| `Status` | int | Trạng thái (xem [Enum TicketStatus](#33-ticketstatus-trạng-thái-phiếu)) |
| `ViolationId` | int? | ID vi phạm liên kết (null nếu phiếu thủ công) |
| `BusinessDate` | datetime? | Ngày nghiệp vụ |
| `ForgivenBy` | int? | ID người gỡ phiếu |
| `ForgivenAt` | datetimeoffset? | Thời điểm gỡ phiếu |
| `ForgiveReason` | string? | Lý do gỡ phiếu |

---

### 1.2. Lấy chi tiết phiếu theo ID
- **Endpoint**: `GET /api/RewardPenalty/{id}`
- **Authorization**: `RewardPenalty.GetById`

**Response (200 OK):**
```json
{
  "Id": 1,
  "Code": "RP-20260312-A1B2C3",
  "UserId": 15,
  "FullName": "Nguyễn Văn A",
  "UserName": "nguyenvana",
  "TicketType": 20,
  "Source": 10,
  "Amount": 500000,
  "Reason": "Vi phạm nội quy",
  "Status": 20,
  "ViolationId": null,
  "BusinessDate": "2026-03-11T00:00:00",
  "ForgivenBy": null,
  "ForgivenAt": null,
  "ForgiveReason": null
}
```

**Response lỗi:**
- `400 Bad Request`: `"ID không hợp lệ"` (khi `id <= 0`)
- `404 Not Found`: `"Không tìm thấy phiếu"`
- `500 Internal Server Error`: `"Lỗi hệ thống: {message}"`

---

### 1.3. Tạo phiếu thủ công
- **Endpoint**: `POST /api/RewardPenalty/Manual`
- **Authorization**: `RewardPenalty.Manual`

**Request Body:**
```json
{
  "UserId": 15,
  "TicketType": 20,
  "Amount": 500000,
  "Reason": "Vi phạm nội quy công ty"
}
```

**Request Parameters:**
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `UserId` | int | ✅ | ID nhân viên bị thưởng/phạt |
| `TicketType` | int | ✅ | Loại phiếu: `10` = Thưởng, `20` = Phạt |
| `Amount` | decimal | ✅ | Số tiền |
| `Reason` | string | ✅ | Lý do thưởng/phạt |

**Response (200 OK):**
```json
{
  "Result": 1,
  "Message": "Thành công"
}
```

**Response lỗi (400 Bad Request):**
```json
{
  "Result": 0,
  "Message": "Mô tả lỗi"
}
```

> [!NOTE]
> **Logic tự động gán:**
> - `Source` luôn được gán = `10` (Manual)
> - `Status` luôn được gán = `20` (PendingApproval)
> - `Code` được tự động sinh theo format `RP-yyyyMMdd-XXXXXX`

---

### 1.4. Duyệt phiếu
- **Endpoint**: `POST /api/RewardPenalty/Approve/{id}`
- **Authorization**: `RewardPenalty.Approve`

**Response (200 OK):**
```json
{
  "Result": 1,
  "Message": "Thành công"
}
```

**Response lỗi (400 Bad Request):**
```json
{
  "Result": 0,
  "Message": "Phiếu không ở trạng thái chờ duyệt"
}
```

> [!IMPORTANT]
> Chỉ phiếu ở trạng thái `20 (PendingApproval)` mới được phép duyệt. Phiếu sau khi duyệt sẽ chuyển sang `30 (Approved)`.

---

### 1.5. Từ chối phiếu
- **Endpoint**: `POST /api/RewardPenalty/Reject/{id}`
- **Authorization**: `RewardPenalty.Reject`

**Response (200 OK):**
```json
{
  "Result": 1,
  "Message": "Thành công"
}
```

**Response lỗi (400 Bad Request):**
```json
{
  "Result": 0,
  "Message": "Phiếu không ở trạng thái chờ duyệt"
}
```

> [!IMPORTANT]
> Chỉ phiếu ở trạng thái `20 (PendingApproval)` mới được phép từ chối. Phiếu sau khi từ chối sẽ chuyển sang `40 (Rejected)`.

---

### 1.6. Gỡ / Hủy phiếu (Amnesty)
- **Endpoint**: `POST /api/RewardPenalty/Cancel/{id}`
- **Authorization**: `RewardPenalty.Cancel`

**Request Body:**
```json
{
  "Reason": "Nhân viên đã giải trình và nộp tiền bổ sung"
}
```

**Request Parameters:**
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `Reason` | string | ✅ | Lý do gỡ biên bản (không được để trống) |

**Response (200 OK):**
```json
{
  "Result": 1,
  "Message": "Thành công"
}
```

**Response lỗi (400 Bad Request):**
```json
{
  "Result": 0,
  "Message": "User đã hết 5 lượt gỡ biên bản trong kỳ này"
}
```

> [!IMPORTANT]
> **Điều kiện hủy phiếu:**
> - Phiếu phải ở trạng thái `20 (PendingApproval)` hoặc `30 (Approved)`.
> - Sau khi hủy, phiếu chuyển sang `50 (Canceled)`.

> [!IMPORTANT]
> **Giới hạn Amnesty cho phiếu tự động:**
> - Mỗi user chỉ được gỡ tối đa `MaxAmnestyPerCycle` lần trong một chu kỳ (theo `ResetPolicy`: Tháng/Quý/Trọn đời).
> - Nếu vượt quá giới hạn, API trả về lỗi: `"User đã hết X lượt gỡ biên bản trong kỳ này"`.
> - Phiếu thủ công (`Source = 10`) không bị giới hạn amnesty.
> - Khi hủy phiếu có liên kết `ViolationId`, vi phạm tương ứng sẽ được chuyển sang trạng thái `30 (Forgiven)`.

---

## 2. Cấu hình Phạt Tự động (AutoPenaltyConfig)

### 2.1. Tìm kiếm danh sách cấu hình
- **Endpoint**: `POST /api/AutoPenaltyConfig/Search`
- **Authorization**: `AutoPenaltyConfig.Search`

**Request Body (SmartTableParam):**
```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 15
  },
  "Search": {
    "PredicateObject": {
      "Keyword": "admin"
    }
  },
  "Sort": {
    "Predicate": "CreatedTime",
    "Reverse": true
  }
}
```

**Request Parameters:**
- `Keyword` (string): Tìm kiếm theo `PauseReason` hoặc `CreatedByUserName`

**Response:**
```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 15,
    "TotalItemCount": 5,
    "TotalPages": 1
  },
  "Items": [
    {
      "Id": 1,
      "IsEnabled": true,
      "IsPaused": false,
      "PauseReason": null,
      "CutoffTimeNextDay": "10:00:00",
      "ResetPolicy": 10,
      "MaxAmnestyPerCycle": 5,
      "EffectiveFrom": "2026-03-01T00:00:00+07:00",
      "EffectiveTo": null,
      "CreatedTime": "2026-03-01T10:00:00+07:00",
      "CreatedByUserName": "admin",
      "LevelCount": 2
    }
  ]
}
```

> [!NOTE]
> Hệ thống luôn sắp xếp theo `CreatedTime` giảm dần (mới nhất trước).

---

### 2.2. Lấy chi tiết cấu hình theo ID
- **Endpoint**: `GET /api/AutoPenaltyConfig/GetById/{id}`
- **Authorization**: `AutoPenaltyConfig.GetById`

**Response (200 OK):**
```json
{
  "Id": 1,
  "IsEnabled": true,
  "IsPaused": false,
  "PauseReason": null,
  "CutoffTimeNextDay": "10:00:00",
  "ResetPolicy": 10,
  "MaxAmnestyPerCycle": 5,
  "Levels": [
    {
      "Id": 1,
      "FromCount": 1,
      "ToCount": 2,
      "ActionType": 10,
      "PenaltyAmount": 0
    },
    {
      "Id": 2,
      "FromCount": 3,
      "ToCount": null,
      "ActionType": 20,
      "PenaltyAmount": 200000
    }
  ]
}
```

**Response Fields:**
| Trường | Kiểu | Mô tả |
|---|---|---|
| `Id` | int | ID cấu hình |
| `IsEnabled` | bool | Bật/tắt tính năng phạt tự động |
| `IsPaused` | bool | Đang tạm dừng hay không |
| `PauseReason` | string? | Lý do tạm dừng |
| `CutoffTimeNextDay` | TimeSpan | Giờ cắt sang ngày hôm sau (format: `HH:mm:ss`) |
| `ResetPolicy` | int | Chu kỳ reset (xem [Enum ResetPolicy](#35-resetpolicy-chu-kỳ-reset)) |
| `MaxAmnestyPerCycle` | int | Số lượt gỡ biên bản tối đa trong một chu kỳ |
| `Levels` | array | Danh sách mức xử lý |

**Levels Fields:**
| Trường | Kiểu | Mô tả |
|---|---|---|
| `Id` | int | ID mức |
| `FromCount` | int | Từ lần vi phạm thứ |
| `ToCount` | int? | Đến lần vi phạm thứ (`null` = không giới hạn) |
| `ActionType` | int | Hình thức xử lý (xem [Enum PenaltyActionType](#36-penaltyactiontype-hình-thức-xử-lý)) |
| `PenaltyAmount` | decimal | Số tiền phạt (= 0 nếu chỉ nhắc nhở) |

**Response lỗi:**
- `400 Bad Request`: `"ID không hợp lệ"` (khi `id <= 0`)
- `404 Not Found`: `"Không tìm thấy cấu hình"`

---

### 2.3. Tạo mới hoặc cập nhật cấu hình
- **Endpoint**: `POST /api/AutoPenaltyConfig/CreateOrUpdate`
- **Authorization**: `AutoPenaltyConfig.CreateOrUpdate`

**Request Body:**
```json
{
  "Id": 0,
  "IsEnabled": true,
  "IsPaused": false,
  "CutoffTimeNextDay": "10:30:00",
  "ResetPolicy": 10,
  "MaxAmnestyPerCycle": 3,
  "ChangeReason": "Tạo cấu hình mới cho quý 2",
  "Levels": [
    {
      "FromCount": 1,
      "ToCount": 1,
      "ActionType": 10,
      "PenaltyAmount": 0
    },
    {
      "FromCount": 2,
      "ToCount": null,
      "ActionType": 20,
      "PenaltyAmount": 200000
    }
  ]
}
```

**Request Parameters:**
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `Id` | int | ✅ | `0` = Tạo mới, `> 0` = Cập nhật theo ID |
| `IsEnabled` | bool | ✅ | Bật/tắt tính năng |
| `IsPaused` | bool | ✅ | Tạm dừng job |
| `CutoffTimeNextDay` | TimeSpan | ✅ | Giờ cắt (format: `HH:mm:ss`) |
| `ResetPolicy` | int | ✅ | Chu kỳ reset |
| `MaxAmnestyPerCycle` | int | ✅ | Số lượt gỡ tối đa |
| `ChangeReason` | string? | ❌ | Lý do thay đổi (ghi vào audit log) |
| `Levels` | array | ✅ | Danh sách mức xử lý |

**Response (200 OK):**
```json
{
  "Result": 1,
  "Message": "Thành công"
}
```

> [!IMPORTANT]
> **Tạo mới (`Id = 0`):** Hệ thống tự gán `EffectiveFrom = Now`, `EffectiveTo = null`.
> **Cập nhật (`Id > 0`):** Levels cũ sẽ bị xóa toàn bộ và thay bằng Levels mới từ request.

> [!NOTE]
> **Audit Log:** Mọi thay đổi cấu hình đều được ghi lại trong `ConfigAuditLog`, bao gồm giá trị cũ, giá trị mới, người thay đổi, và lý do.

---

### 2.4. Xóa cấu hình
- **Endpoint**: `DELETE /api/AutoPenaltyConfig/Delete/{id}`
- **Authorization**: `AutoPenaltyConfig.Delete`

**Response (200 OK):**
```json
{
  "Result": 1,
  "Message": "Thành công"
}
```

**Response lỗi:**
- `400 Bad Request`: `"ID không hợp lệ"` hoặc `"Không tìm thấy cấu hình"`

> [!NOTE]
> Xóa cấu hình sử dụng cơ chế soft delete (`IsDeleted = true`), dữ liệu không bị xóa vĩnh viễn.

---

### 2.5. Lấy cấu hình đang hiệu lực
- **Endpoint**: `GET /api/AutoPenaltyConfig`
- **Authorization**: `AutoPenaltyConfig.GetCurrentConfig`

**Response (200 OK):**
```json
{
  "Id": 1,
  "IsEnabled": true,
  "IsPaused": false,
  "PauseReason": null,
  "CutoffTimeNextDay": "10:00:00",
  "ResetPolicy": 10,
  "MaxAmnestyPerCycle": 5,
  "Levels": [
    {
      "Id": 1,
      "FromCount": 1,
      "ToCount": 2,
      "ActionType": 10,
      "PenaltyAmount": 0
    },
    {
      "Id": 2,
      "FromCount": 3,
      "ToCount": null,
      "ActionType": 20,
      "PenaltyAmount": 200000
    }
  ]
}
```

**Response lỗi:**
- `404 Not Found`: `"Chưa có cấu hình nào"`

> [!NOTE]
> Endpoint này lấy cấu hình mới nhất có `EffectiveTo = null` (đang hiệu lực). Khác với `GetById`, endpoint này không cần truyền ID.

---

### 2.6. Cập nhật cấu hình (Versioning)
- **Endpoint**: `PUT /api/AutoPenaltyConfig`
- **Authorization**: `AutoPenaltyConfig.UpdateConfig`

**Request Body:**
```json
{
  "IsEnabled": true,
  "IsPaused": false,
  "CutoffTimeNextDay": "10:30:00",
  "ResetPolicy": 10,
  "MaxAmnestyPerCycle": 3,
  "ChangeReason": "Cập nhật định mức phạt mới",
  "Levels": [
    {
      "FromCount": 1,
      "ToCount": 1,
      "ActionType": 10,
      "PenaltyAmount": 0
    },
    {
      "FromCount": 2,
      "ToCount": null,
      "ActionType": 20,
      "PenaltyAmount": 200000
    }
  ]
}
```

**Response (200 OK):**
```json
{
  "Result": 1,
  "Message": "Thành công"
}
```

> [!IMPORTANT]
> **Cơ chế Versioning:** Endpoint này đóng cấu hình hiện tại (gán `EffectiveTo`) và tạo một bản ghi mới. Khác với `CreateOrUpdate`, nó luôn tạo version mới thay vì cập nhật in-place.

---

### 2.7. Tạm dừng / Mở lại Job
- **Endpoint**: `PUT /api/AutoPenaltyConfig/Pause`
- **Authorization**: `AutoPenaltyConfig.TogglePause`

**Request Body:**
```json
{
  "IsPaused": true,
  "PauseReason": "Bảo trì hệ thống"
}
```

**Request Parameters:**
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| `IsPaused` | bool | ✅ | `true` = Tạm dừng, `false` = Mở lại |
| `PauseReason` | string? | ❌ | Lý do tạm dừng (chỉ lưu khi `IsPaused = true`) |

**Response (200 OK):**
```json
{
  "Result": 1,
  "Message": "Thành công"
}
```

**Response lỗi (400 Bad Request):**
```json
{
  "Result": 0,
  "Message": "Chưa có cấu hình nào"
}
```

> [!NOTE]
> Khi mở lại (`IsPaused = false`), `PauseReason` sẽ tự động được xóa (set null).

---

## 3. Phụ lục Enum

### 3.1. TicketType (Loại phiếu)
| Giá trị | Tên | Mô tả |
|---|---|---|
| `10` | Reward | Thưởng |
| `20` | Penalty | Phạt |

### 3.2. TicketSource (Nguồn gốc)
| Giá trị | Tên | Mô tả |
|---|---|---|
| `10` | Manual | Thủ công |
| `20` | Automated | Tự động |

### 3.3. TicketStatus (Trạng thái phiếu)
| Giá trị | Tên | Mô tả |
|---|---|---|
| `10` | Draft | Nháp |
| `20` | PendingApproval | Chờ duyệt |
| `30` | Approved | Đã duyệt |
| `40` | Rejected | Từ chối |
| `50` | Canceled | Đã hủy |

### 3.4. ViolationStatus (Trạng thái vi phạm)
| Giá trị | Tên | Mô tả |
|---|---|---|
| `10` | Active | Mới |
| `20` | TicketCreated | Đã tạo phiếu |
| `30` | Forgiven | Đã gỡ |

### 3.5. ResetPolicy (Chu kỳ reset)
| Giá trị | Tên | Mô tả |
|---|---|---|
| `10` | Monthly | Hàng tháng |
| `20` | Quarterly | Hàng quý |
| `30` | Lifetime | Trọn đời |

### 3.6. PenaltyActionType (Hình thức xử lý)
| Giá trị | Tên | Mô tả |
|---|---|---|
| `10` | Remind | Nhắc nhở |
| `20` | Penalty | Phạt tiền |
