# API Chuyển Khoản Ví Điện Tử

## Tổng quan

API này cho phép người dùng chuyển tiền từ ví điện tử của mình sang ví của người dùng khác trong hệ thống. Ví nguồn sẽ được tự động lấy từ tài khoản người dùng đang đăng nhập, đảm bảo tính bảo mật cao.

---

## Endpoint

```
POST /api/Wallet/Transfer
```

**Authorization**: Bearer Token (JWT)

**Role Required**: `Wallet.Transfer`

---

## Request

### Headers

| Header | Value | Required |
|--------|-------|----------|
| `Authorization` | `Bearer {token}` | ✅ |
| `Content-Type` | `multipart/form-data` | ✅ |

### Request Body

**Format**: `multipart/form-data`

```
DestinationWalletId: 123
Amount: 500000
Note: Chuyển tiền mua hàng
RequestId: 550e8400-e29b-41d4-a716-446655440000
ProofImage: [File] proof.jpg
```

### Request Model: `TransferWalletModel`

| Thuộc tính | Kiểu dữ liệu | Bắt buộc | Ràng buộc | Mô tả |
|------------|-------------|----------|-----------|-------|
| `DestinationWalletId` | `int` | ✅ | > 0 | ID ví đích (ví nhận tiền) |
| `Amount` | `decimal` | ✅ | > 0 | Số tiền cần chuyển |
| `Note` | `string` | ❌ | Max 500 ký tự | Ghi chú cho giao dịch |
| `RequestId` | `Guid?` | ❌ | - | Request ID để đảm bảo idempotency (tránh chuyển tiền trùng khi retry). Nếu không truyền, hệ thống sẽ tự tạo |
| `ProofImage` | `IFormFile` | ✅ | File ảnh | Ảnh chứng từ giao dịch (JPG, PNG, etc.) |

> [!IMPORTANT]
> - Ví nguồn (`SourceWalletId`) sẽ được **tự động lấy từ người dùng đang đăng nhập**, không cần và không được phép truyền trong request
> - Điều này đảm bảo người dùng chỉ có thể chuyển tiền từ ví của chính họ
> - `UpdatedBy` và `UpdatedByUserName` sẽ được tự động lấy từ JWT token

---

## Response

### Success Response (200 OK)

```json
{
  "Result": 1,
  "Message": "Chuyển tiền thành công 500,000 VND từ Nguyễn Văn A đến Trần Thị B",
  "TransferGroupId": "550e8400-e29b-41d4-a716-446655440000",
  "SourceTransactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "DestTransactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae8",
  "SourceWalletNewBalance": 1500000,
  "DestWalletNewBalance": 2500000
}
```

### Response Model: `WalletTransferResult`

| Thuộc tính | Kiểu dữ liệu | Giá trị | Mô tả |
|------------|-------------|---------|-------|
| `Result` | `int` (enum) | `1` = Success<br>`2` = Failed<br>`3` = SystemError | Kết quả thực hiện |
| `Message` | `string` | - | Thông báo kết quả |
| `TransferGroupId` | `Guid?` | - | ID nhóm giao dịch (dùng để tra cứu 2 dòng transaction) |
| `SourceTransactionId` | `Guid?` | - | ID giao dịch bên ví nguồn |
| `DestTransactionId` | `Guid?` | - | ID giao dịch bên ví đích |
| `SourceWalletNewBalance` | `decimal?` | - | Số dư mới của ví nguồn sau khi chuyển |
| `DestWalletNewBalance` | `decimal?` | - | Số dư mới của ví đích sau khi chuyển |

---

## Error Responses

### 401 Unauthorized
Người dùng chưa đăng nhập hoặc token không hợp lệ.

```json
{
  "message": "Unauthorized"
}
```

### 403 Forbidden
Người dùng không có quyền `Wallet.Transfer`.

```json
{
  "message": "User is not in role 'Wallet.Transfer'"
}
```

### 400 Bad Request - Không tìm thấy ví

```json
{
  "message": "Không tìm thấy ví của người dùng"
}
```

### 400 Bad Request - Validation Error

```json
{
  "Result": 2,
  "Message": "Số tiền chuyển phải lớn hơn 0",
  "TransferGroupId": null,
  "SourceTransactionId": null,
  "DestTransactionId": null,
  "SourceWalletNewBalance": null,
  "DestWalletNewBalance": null
}
```

**Các lỗi validation khác**:
- `"ID ví đích không hợp lệ"`
- `"Ví nguồn và ví đích không được trùng nhau"`
- `"Số dư ví nguồn không đủ. Số dư hiện tại: {balance} VND, cần: {amount} VND"`

### 400 Bad Request - Business Logic Error

```json
{
  "Result": 2,
  "Message": "Ví nguồn không ở trạng thái hoạt động",
  "TransferGroupId": null,
  "SourceTransactionId": null,
  "DestTransactionId": null,
  "SourceWalletNewBalance": null,
  "DestWalletNewBalance": null
}
```

**Các lỗi business logic khác**:
- `"Ví đích không tồn tại"`
- `"Ví đích không ở trạng thái hoạt động"`
- `"Giao dịch chuyển tiền này đã được thực hiện trước đó"` (Idempotency check)
- `"Giao dịch bị xung đột do có người khác đang thao tác. Vui lòng thử lại sau."`

### 500 Internal Server Error

```json
{
  "Result": 3,
  "Message": "Lỗi hệ thống: {error details}",
  "TransferGroupId": null,
  "SourceTransactionId": null,
  "DestTransactionId": null,
  "SourceWalletNewBalance": null,
  "DestWalletNewBalance": null
}
```

---

## Ví dụ sử dụng

### Ví dụ 1: Chuyển tiền cơ bản

**Request**:
```bash
POST /api/Wallet/Transfer
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: multipart/form-data

DestinationWalletId: 123
Amount: 500000
Note: Chuyển tiền mua hàng
ProofImage: [File upload]
```

**Response** (Success):
```json
{
  "Result": 1,
  "Message": "Chuyển tiền thành công 500,000 VND từ Nguyễn Văn A đến Trần Thị B",
  "TransferGroupId": "550e8400-e29b-41d4-a716-446655440000",
  "SourceTransactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "DestTransactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae8",
  "SourceWalletNewBalance": 1500000,
  "DestWalletNewBalance": 2500000
}
```

### Ví dụ 2: Chuyển tiền với RequestId (Idempotency)

**Request**:
```bash
POST /api/Wallet/Transfer
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: multipart/form-data

DestinationWalletId: 456
Amount: 1000000
Note: Thanh toán hóa đơn #12345
RequestId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
ProofImage: [File upload]
```

### Ví dụ 3: Lỗi - Số dư không đủ

**Request**:
```
DestinationWalletId: 123
Amount: 10000000
Note: Chuyển tiền
ProofImage: [File upload]
```

**Response** (400 Bad Request):
```json
{
  "Result": 2,
  "Message": "Số dư ví nguồn không đủ. Số dư hiện tại: 1,500,000 VND, cần: 10,000,000 VND",
  "TransferGroupId": null,
  "SourceTransactionId": null,
  "DestTransactionId": null,
  "SourceWalletNewBalance": null,
  "DestWalletNewBalance": null
}
```

---

## Code Examples

### JavaScript (Fetch API)

```javascript
const transferMoney = async (destinationWalletId, amount, note, proofImageFile, requestId = null) => {
  const formData = new FormData();
  formData.append('DestinationWalletId', destinationWalletId);
  formData.append('Amount', amount);
  formData.append('Note', note || '');
  formData.append('ProofImage', proofImageFile);
  if (requestId) {
    formData.append('RequestId', requestId);
  }

  const response = await fetch('/api/Wallet/Transfer', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`
      // Không set Content-Type, browser sẽ tự set với boundary
    },
    body: formData
  });

  const result = await response.json();

  if (!response.ok) {
    throw new Error(result.Message || 'Transfer failed');
  }

  return result;
};

// Sử dụng
try {
  // Lấy file từ input element
  const fileInput = document.querySelector('#proofImageInput');
  const proofImage = fileInput.files[0];
  
  const result = await transferMoney(123, 500000, 'Chuyển tiền mua hàng', proofImage);
  
  if (result.Result === 1) { // Success
    console.log('Chuyển tiền thành công!');
    console.log(`Số dư mới: ${result.SourceWalletNewBalance.toLocaleString()} VND`);
  } else {
    console.error('Lỗi:', result.Message);
  }
} catch (error) {
  console.error('Lỗi:', error.message);
}
```

### C# (HttpClient)

```csharp
public async Task<WalletTransferResult> TransferMoneyAsync(
    int destinationWalletId, 
    decimal amount,
    Stream proofImageStream,
    string proofImageFileName,
    string note = null,
    Guid? requestId = null)
{
    using var formData = new MultipartFormDataContent();
    
    formData.Add(new StringContent(destinationWalletId.ToString()), "DestinationWalletId");
    formData.Add(new StringContent(amount.ToString()), "Amount");
    formData.Add(new StringContent(note ?? string.Empty), "Note");
    
    if (requestId.HasValue)
    {
        formData.Add(new StringContent(requestId.Value.ToString()), "RequestId");
    }
    
    var imageContent = new StreamContent(proofImageStream);
    imageContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
    formData.Add(imageContent, "ProofImage", proofImageFileName);

    var response = await _httpClient.PostAsync("/api/Wallet/Transfer", formData);
    var result = await response.Content.ReadFromJsonAsync<WalletTransferResult>();

    if (result.Result != Result.Success)
    {
        throw new Exception($"Transfer failed: {result.Message}");
    }

    return result;
}

// Sử dụng
try
{
    using var fileStream = File.OpenRead("proof.jpg");
    var result = await TransferMoneyAsync(
        123, 
        500000, 
        fileStream,
        "proof.jpg",
        "Chuyển tiền mua hàng"
    );
    Console.WriteLine($"Chuyển tiền thành công! Số dư mới: {result.SourceWalletNewBalance:N0} VND");
}
catch (Exception ex)
{
    Console.WriteLine($"Lỗi: {ex.Message}");
}
```

### cURL

```bash
curl -X POST "https://api.example.com/api/Wallet/Transfer" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -F "DestinationWalletId=123" \
  -F "Amount=500000" \
  -F "Note=Chuyển tiền mua hàng" \
  -F "ProofImage=@/path/to/proof.jpg"
```

---

## Luồng xử lý

```mermaid
sequenceDiagram
    participant Client
    participant API
    participant Auth
    participant WalletService
    participant TransferService
    participant Database

    Client->>API: POST /api/Wallet/Transfer
    API->>Auth: Xác thực token
    Auth-->>API: User info
    
    API->>WalletService: GetWalletByUserId(userId)
    WalletService->>Database: Query wallet by userId
    Database-->>WalletService: Wallet info
    WalletService-->>API: Source wallet
    
    alt Không tìm thấy ví
        API-->>Client: 400 Bad Request
    end
    
    API->>TransferService: Transfer(sourceWalletId, model)
    TransferService->>TransferService: Validate request
    
    alt Validation failed
        TransferService-->>API: Validation error
        API-->>Client: 400 Bad Request
    end
    
    TransferService->>Database: Begin Transaction
    TransferService->>Database: Check idempotency
    
    alt Duplicate request
        TransferService-->>API: Duplicate error
        API-->>Client: 400 Bad Request
    end
    
    TransferService->>Database: Load wallets (with lock)
    TransferService->>TransferService: Validate wallets & balance
    
    alt Business validation failed
        TransferService->>Database: Rollback
        TransferService-->>API: Business error
        API-->>Client: 400 Bad Request
    end
    
    TransferService->>Database: Create source transaction
    TransferService->>Database: Create dest transaction
    TransferService->>Database: Update source wallet balance
    TransferService->>Database: Update dest wallet balance
    TransferService->>Database: Commit Transaction
    
    TransferService-->>API: Success result
    API-->>Client: 200 OK with result
```

---

## Tính năng đặc biệt

### 1. Bảo mật cao
- ✅ Ví nguồn được lấy tự động từ người dùng đang đăng nhập
- ✅ Người dùng không thể giả mạo để chuyển tiền từ ví của người khác
- ✅ Yêu cầu role `Wallet.Transfer` để thực hiện giao dịch
- ✅ Thông tin `UpdatedBy` được lấy từ JWT token

### 2. Idempotency (Tránh trùng lặp)
- ✅ Sử dụng `RequestId` để đảm bảo không tạo giao dịch trùng lặp khi retry
- ✅ Nếu request với cùng `RequestId` đã được xử lý, hệ thống sẽ trả về lỗi
- ✅ Tự động tạo `RequestId` nếu client không truyền

### 3. Transaction Safety
- ✅ Sử dụng Database Transaction để đảm bảo tính toàn vẹn dữ liệu
- ✅ Optimistic Concurrency Control với RowVersion
- ✅ Retry mechanism với exponential backoff (tối đa 3 lần)
- ✅ Deadlock prevention bằng cách lock ví theo thứ tự ID

### 4. Audit Trail
- ✅ Tạo 2 transaction records: một cho ví nguồn (Outcome), một cho ví đích (Income)
- ✅ Lưu trữ đầy đủ thông tin: người thực hiện, thời gian, số tiền, ghi chú
- ✅ Sử dụng `TransferGroupId` để liên kết 2 transaction với nhau

---

## Lấy danh sách ví đích (Destination Wallet)

Khi xây dựng giao diện chuyển tiền, bạn cần cho phép người dùng chọn ví đích. Sử dụng API **GetSuggestions** để lấy danh sách ví phục vụ cho select control/autocomplete.

### Endpoint

```
GET /api/Wallet/GetSuggestions?keyword={keyword}
```

**Authorization**: Bearer Token (JWT)

**Role Required**: `Wallet.GetSuggestions`

### Request Parameters

| Tham số | Kiểu | Bắt buộc | Mô tả |
|---------|------|----------|-------|
| `keyword` | `string` | ❌ | Từ khóa tìm kiếm (tên, số điện thoại, username). Nếu không truyền, sẽ trả về 20 ví đầu tiên |

### Response

Trả về danh sách tối đa **20 ví** dạng `WalletItem[]`:

```json
[
  {
    "Id": 123,
    "UserId": 456,
    "UserName": "nguyenvana",
    "FullName": "Nguyễn Văn A",
    "WalletType": "Employee",
    "Balance": "1,500,000",
    "WalletStatus": "Active"
  },
  {
    "Id": 124,
    "UserId": 457,
    "UserName": "tranthib",
    "FullName": "Trần Thị B",
    "WalletType": "Customer",
    "Balance": "2,000,000",
    "WalletStatus": "Active"
  }
]
```

### Ví dụ sử dụng

#### JavaScript (Autocomplete/Select)

```javascript
// Hàm lấy danh sách ví suggestions
const getWalletSuggestions = async (keyword) => {
  const response = await fetch(`/api/Wallet/GetSuggestions?keyword=${encodeURIComponent(keyword || '')}`, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  if (!response.ok) {
    throw new Error('Failed to fetch wallet suggestions');
  }

  return await response.json();
};

// Sử dụng với autocomplete
const searchInput = document.querySelector('#walletSearch');
searchInput.addEventListener('input', async (e) => {
  const keyword = e.target.value;
  
  if (keyword.length >= 2) { // Chỉ search khi nhập >= 2 ký tự
    const wallets = await getWalletSuggestions(keyword);
    
    // Hiển thị suggestions
    displaySuggestions(wallets);
  }
});

function displaySuggestions(wallets) {
  const suggestionList = document.querySelector('#suggestionList');
  suggestionList.innerHTML = wallets.map(wallet => `
    <li data-wallet-id="${wallet.Id}">
      <strong>${wallet.FullName}</strong> (${wallet.UserName})
      <br>
      <small>Số dư: ${wallet.Balance} VND</small>
    </li>
  `).join('');
}
```

#### Vue.js với v-select

```vue
<template>
  <v-select
    v-model="selectedWallet"
    :options="walletOptions"
    :filterable="false"
    @search="onSearch"
    label="FullName"
    placeholder="Tìm ví đích..."
  >
    <template #option="{ FullName, UserName, Balance }">
      <div>
        <strong>{{ FullName }}</strong> ({{ UserName }})
        <br>
        <small>Số dư: {{ Balance }} VND</small>
      </div>
    </template>
  </v-select>
</template>

<script>
import { debounce } from 'lodash';

export default {
  data() {
    return {
      selectedWallet: null,
      walletOptions: []
    };
  },
  methods: {
    onSearch: debounce(async function(keyword) {
      if (keyword.length >= 2) {
        const response = await fetch(
          `/api/Wallet/GetSuggestions?keyword=${encodeURIComponent(keyword)}`,
          {
            headers: {
              'Authorization': `Bearer ${this.$store.state.token}`
            }
          }
        );
        this.walletOptions = await response.json();
      }
    }, 300) // Debounce 300ms
  }
};
</script>
```

> [!TIP]
> - Sử dụng **debounce** để tránh gọi API quá nhiều khi người dùng đang gõ
> - Chỉ bắt đầu search khi người dùng nhập ít nhất 2-3 ký tự
> - API tự động giới hạn 20 kết quả để tối ưu performance
> - Tìm kiếm theo **FullName**, **PhoneNumber**, và **UserName**

---

## Best Practices

### 1. Kiểm tra Result
Luôn kiểm tra `Result` trong response:

```javascript
const result = await transferMoney(123, 500000, 'Test');

switch (result.Result) {
  case 1: // Success
    console.log(`Thành công! Số dư mới: ${result.SourceWalletNewBalance}`);
    break;
  case 2: // Failed
    console.error(`Thất bại: ${result.Message}`);
    break;
  case 3: // SystemError
    console.error(`Lỗi hệ thống: ${result.Message}`);
    break;
}
```

### 2. Retry với Idempotency
Khi retry, sử dụng cùng một `RequestId`:

```javascript
const requestId = crypto.randomUUID();
const maxRetries = 3;

for (let i = 0; i < maxRetries; i++) {
  try {
    const result = await transferMoney(123, 500000, 'Test', requestId);
    if (result.Result === 1) {
      console.log('Thành công!');
      break;
    } else {
      throw new Error(result.Message);
    }
  } catch (error) {
    if (i === maxRetries - 1) throw error;
    await sleep(1000 * Math.pow(2, i)); // Exponential backoff
  }
}
```

### 3. Validation phía client
Validate dữ liệu trước khi gửi request:

```javascript
function validateTransfer(destinationWalletId, amount) {
  if (!destinationWalletId || destinationWalletId <= 0) {
    throw new Error('ID ví đích không hợp lệ');
  }
  if (!amount || amount <= 0) {
    throw new Error('Số tiền phải lớn hơn 0');
  }
}
```

### 4. Xử lý lỗi concurrency
```javascript
async function transferWithRetry(destWalletId, amount, note) {
  const maxRetries = 3;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      const result = await transferMoney(destWalletId, amount, note);
      return result;
    } catch (error) {
      // Retry nếu là lỗi concurrency
      if (error.message.includes('xung đột') && i < maxRetries - 1) {
        await sleep(500 * (i + 1));
        continue;
      }
      throw error;
    }
  }
}
```

---

## Changelog

### Version 3.0 (Current - 2024-12-19)
- ✅ **Breaking Change**: Thêm trường `ProofImage` bắt buộc
- ✅ **Breaking Change**: Chuyển từ `application/json` sang `multipart/form-data`
- ✅ Lưu trữ ảnh chứng từ cho mỗi giao dịch chuyển tiền
- ✅ Migration thêm cột `MediaId` vào bảng `Transaction`

### Version 2.0 (2024-12-17)
- ✅ **Breaking Change**: Loại bỏ `SourceWalletId` từ request body
- ✅ Ví nguồn được lấy tự động từ người dùng đang đăng nhập
- ✅ Tăng cường bảo mật, ngăn chặn giả mạo ví nguồn
- ✅ Tất cả tham số JSON theo **PascalCase**
- ✅ `Result` là **số** (enum: 1=Success, 2=Failed, 3=SystemError)

### Version 1.0 (Legacy)
- ❌ Yêu cầu client truyền `SourceWalletId` trong request body
- ❌ Có rủi ro bảo mật nếu client truyền sai ID
- ❌ Sử dụng camelCase
- ❌ `Result` là string

---

## Related APIs

- [POST /api/Wallet/BalanceAdjustment/{walletId}](./API_Wallet_BalanceAdjustment.md) - Điều chỉnh số dư ví (chỉ Founder)
- [GET /api/Wallet/GetMyWallet](./API_Wallet_GetMyWallet.md) - Lấy thông tin ví của user hiện tại
- [GET /api/Wallet/GetById](./API_Wallet_GetById.md) - Lấy thông tin ví theo ID
- [POST /api/Transaction/SearchWalletTransactions](./API_Transaction_Search.md) - Xem lịch sử giao dịch
