> ## Documentation Index
> Fetch the complete documentation index at: https://docs.econtractid.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Đồng bộ người dùng

> Tạo, cập nhật, khoá và tra cứu người dùng từ hệ thống nhân sự, đồng bộ theo mã nhân sự

Sáu endpoint để hệ thống nhân sự (HRM) giữ danh sách người dùng eContract khớp với danh sách nhân sự của mình.

| # | Endpoint                                             | Mục đích                    |
| - | ---------------------------------------------------- | --------------------------- |
| 1 | `GET /api/integration/directory/users`               | Danh sách người dùng        |
| 2 | `GET /api/integration/directory/users/{code}`        | Chi tiết một người          |
| 3 | `POST /api/integration/directory/users`              | Tạo người dùng              |
| 4 | `PUT /api/integration/directory/users/{code}`        | Cập nhật hồ sơ và phân công |
| 5 | `PUT /api/integration/directory/users/{code}/status` | Khoá hoặc bật lại tài khoản |
| 6 | `POST /api/integration/directory/users/bulk`         | Đồng bộ hàng loạt           |

Xác thực và quy tắc `tenantCode` giống hệt [API Danh mục](/api-reference/endpoint/danh-muc-to-chuc-vai-tro).

***

## Mã người dùng là khoá đồng bộ

Mọi endpoint ở đây định danh người dùng bằng **`code`** — với người dùng do hệ thống nhân sự đẩy sang, đó chính là mã nhân viên bên bạn.

| Tính chất               | Chi tiết                                                                                 |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| Phạm vi duy nhất        | Trong **một tenant**. Hai công ty cùng đánh số `NV001` là hợp lệ                         |
| Ai sở hữu               | Hệ thống nhân sự. Giao diện web econtractid chỉ hiển thị, không cho sửa                  |
| Vì sao không dùng email | Email đổi được ở phía nhân sự; mã nhân viên thì không. Khoá đồng bộ phải là thứ đứng yên |
| Tối đa                  | 64 ký tự                                                                                 |

<Note>
  `code` là **một trường duy nhất** cho mọi người dùng, không phải trường riêng của tích hợp nhân sự. Người dùng bên ngoài (đối tác ký) cũng mang `code`, nhưng do econtractid tự sinh theo định dạng riêng. Hai nhóm người dùng tách biệt nên không bao giờ giành nhau một mã.
</Note>

<Warning>
  **`email` và `username` cố định sau khi tạo.** Gửi giá trị khác trong request cập nhật sẽ nhận `409` `IDENTITY_IMMUTABLE` chứ không bị bỏ qua im lặng — đổi định danh đăng nhập là đổi tài khoản, không phải cập nhật hồ sơ. Gửi lại đúng giá trị hiện tại (kể cả khác hoa thường) thì được chấp nhận, để bạn cứ gửi nguyên bản ghi mỗi lần đồng bộ.
</Warning>

***

## 1. Danh sách người dùng

```http theme={null}
GET /api/integration/directory/users
```

### Query

| Tham số                                           | Kiểu      | Mặc định | Mô tả                                                                            |
| ------------------------------------------------- | --------- | -------- | -------------------------------------------------------------------------------- |
| `tenantCode`                                      | `string`  | —        | Bắt buộc với key toàn cục                                                        |
| `code`                                            | `string`  | —        | Khớp tuyệt đối                                                                   |
| `username` / `email` / `phone` / `identityNumber` | `string`  | —        | Khớp tuyệt đối                                                                   |
| `organizationCode`                                | `string`  | —        | Chỉ lấy người thuộc đơn vị này                                                   |
| `roleCode`                                        | `string`  | —        | Chỉ lấy người giữ vai trò có mã này                                              |
| `roleName`                                        | `string`  | —        | Chỉ lấy người giữ vai trò có tên này — dùng khi mọi vai trò cùng một mã mặc định |
| `status`                                          | `integer` | —        | `1` chờ, `2` hoạt động, `3` khoá, `4` chờ kích hoạt                              |
| `hasCode`                                         | `boolean` | —        | `false` = chỉ người **chưa** có mã                                               |
| `q`                                               | `string`  | —        | Tìm theo tên, email, tên đăng nhập hoặc mã nhân sự                               |
| `updatedFrom` / `updatedTo`                       | `string`  | —        | ISO 8601                                                                         |
| `page`                                            | `integer` | `1`      |                                                                                  |
| `pageSize`                                        | `integer` | `50`     | Tối đa `200`                                                                     |

<Note>
  `hasCode=false` là cách rà những tài khoản được tạo thẳng trên giao diện web mà hệ thống nhân sự chưa biết. Đối chiếu nhóm này trước khi chạy đồng bộ hàng loạt, tránh tạo trùng người.
</Note>

<Note>
  `updatedFrom` cho phép đồng bộ tăng dần: mỗi lần chạy chỉ kéo về phần đã đổi kể từ lần chạy trước, thay vì tải lại toàn bộ danh sách.
</Note>

### Response

```json theme={null}
{
  "items": [
    {
      "code": "NV001",
      "username": "an.nguyen@acme.vn",
      "name": "Nguyễn Văn An",
      "email": "an.nguyen@acme.vn",
      "phone": "0901234567",
      "identityNumber": "079201001234",
      "taxCode": null,
      "address": "12 Nguyễn Huệ, Q1",
      "gender": "male",
      "status": 2,
      "type": "internal",
      "members": [
        {
          "organizationCode": "ACME-IT",
          "organizationName": "Phòng Công nghệ thông tin",
          "roleCode": "NHANVIEN",
          "roleName": "Nhân viên"
        }
      ],
      "createdAt": "2026-09-15T08:12:30.000Z",
      "updatedAt": "2026-09-15T08:12:30.000Z"
    }
  ],
  "page": 1,
  "pageSize": 50,
  "total": 1
}
```

***

## 2. Chi tiết một người

```http theme={null}
GET /api/integration/directory/users/{code}?tenantCode=ACME
```

Trả về đúng một object như phần tử trong `items` ở trên. Không có mã đó trong tenant → `404` `USER_CODE_NOT_FOUND`.

***

## 3. Tạo người dùng

```http theme={null}
POST /api/integration/directory/users
```

### Body

```json theme={null}
{
  "tenantCode": "ACME",
  "code": "NV001",
  "name": "Nguyễn Văn An",
  "email": "an.nguyen@acme.vn",
  "username": "an.nguyen@acme.vn",
  "phone": "0901234567",
  "identityNumber": "079201001234",
  "address": "12 Nguyễn Huệ, Q1",
  "gender": "male",
  "members": [
    { "organizationCode": "ACME-IT", "roleName": "Nhân viên" }
  ]
}
```

| Trường     | Bắt buộc | Mô tả                                                                                                             |
| ---------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `code`     | ✅        | Mã nhân viên, tối đa 64 ký tự                                                                                     |
| `email`    | ✅        | Định danh đăng nhập, cố định sau khi tạo                                                                          |
| `username` | —        | Bỏ trống → lấy theo email. Cố định sau khi tạo                                                                    |
| `members`  | —        | Mỗi đơn vị **chỉ một** vai trò. Tối đa 50 dòng. Mỗi dòng cần `organizationCode` và **`roleCode` hoặc `roleName`** |

<Warning>
  **Dùng `roleName`, không phải `roleCode`.** Trong nhiều tổ chức, mọi vai trò dùng chung một mã mặc định (`role`), nên `roleCode` không chỉ ra được vai trò nào — API sẽ trả `400 ROLE_AMBIGUOUS_IN_ORGANIZATION`. Tên vai trò thì duy nhất trong một đơn vị, nên đó là cách định danh đáng tin. Gọi [`GET /directory/roles`](/api-reference/endpoint/danh-muc-to-chuc-vai-tro) để lấy đúng tên.

  Gửi cả hai cũng được, nhưng chúng phải cùng chỉ về một vai trò — mâu thuẫn thì bị từ chối chứ không lặng lẽ chọn một bên.
</Warning>

Người dùng được tạo với `type = internal`, đặt ở gốc tenant, và ở trạng thái **chờ đặt mật khẩu** — họ tự đặt mật khẩu qua luồng quên mật khẩu của econtractid.

### Response

HTTP `201`; thân phản hồi là object người dùng như mục 1 (trường `statusCode` trong envelope luôn là `200`, giống mọi endpoint khác của hệ thống).

***

## 4. Cập nhật

```http theme={null}
PUT /api/integration/directory/users/{code}
```

Cập nhật **từng phần**: chỉ trường có mặt trong body mới bị ghi đè.

```json theme={null}
{
  "tenantCode": "ACME",
  "name": "Nguyễn Văn An",
  "phone": "0907654321",
  "members": [
    { "organizationCode": "ACME-KD", "roleName": "Trưởng phòng" }
  ]
}
```

| Hành vi                  | Kết quả                                                            |
| ------------------------ | ------------------------------------------------------------------ |
| Không gửi `members`      | Phân công **giữ nguyên**                                           |
| Gửi `members` có dữ liệu | Thay thế toàn bộ phân công                                         |
| Gửi `members: []`        | **Không xoá gì** — mảng rỗng được hiểu là "không có gì để đồng bộ" |

<Warning>
  Không có cách nào xoá sạch phân công của một người qua API này. Đó là chủ ý: một payload lỡ thiếu trường không được phép thổi bay vai trò của người đang làm việc. Cần gỡ vai trò thì thao tác trên giao diện quản trị.
</Warning>

***

## 5. Khoá / bật lại tài khoản

```http theme={null}
PUT /api/integration/directory/users/{code}/status
```

```json theme={null}
{ "tenantCode": "ACME", "status": 3 }
```

| `status` | Ý nghĩa        |
| -------- | -------------- |
| `2`      | Đang hoạt động |
| `3`      | Khoá           |

Chỉ nhận hai giá trị này; các trạng thái nội bộ khác không đặt được từ bên ngoài.

<Note>
  **Không có endpoint xoá người dùng.** Nhân sự nghỉ việc thì khoá tài khoản — hồ sơ họ đã ký vẫn phải truy vết được, nên bản ghi người dùng không được biến mất.
</Note>

***

## 6. Đồng bộ hàng loạt

```http theme={null}
POST /api/integration/directory/users/bulk
```

Upsert theo `code`: chưa có thì tạo, đã có thì cập nhật. **Tối đa 200 dòng** mỗi request.

```json theme={null}
{
  "tenantCode": "ACME",
  "users": [
    {
      "code": "NV001",
      "name": "Nguyễn Văn An",
      "email": "an.nguyen@acme.vn",
      "members": [{ "organizationCode": "ACME-IT", "roleCode": "NHANVIEN" }]
    },
    {
      "code": "NV002",
      "name": "Trần Thị Bình",
      "email": "binh.tran@acme.vn"
    }
  ]
}
```

### Response

Luôn `200` khi request hợp lệ — lỗi nằm ở **từng dòng**, không phải cả lần gọi.

```json theme={null}
{
  "total": 3,
  "created": 1,
  "updated": 1,
  "failed": 1,
  "results": [
    { "code": "NV001", "action": "created" },
    { "code": "NV002", "action": "updated" },
    {
      "code": "NV003",
      "action": "failed",
      "errorCode": "EMAIL_ALREADY_EXISTS",
      "message": "email already exists: cu@acme.vn"
    }
  ]
}
```

<Warning>
  **Vắng mặt trong payload không có nghĩa là đã nghỉ việc.** Danh sách gửi lên chỉ là phần bạn muốn đồng bộ; người không có trong đó không bị đụng tới, không bị khoá, không bị xoá. Muốn khoá ai thì gọi endpoint đổi trạng thái.
</Warning>

<Note>
  Mỗi dòng chạy trong một transaction riêng nên một dòng hỏng không kéo theo dòng đã ghi. Ngược lại, **trùng `code` ngay trong một payload** làm hỏng cả request (`400` `USER_CODE_DUPLICATED_IN_PAYLOAD`) và chưa ghi dòng nào — nếu xử lý tiếp thì dòng sau sẽ ghi đè dòng trước và kết quả phụ thuộc thứ tự mảng.
</Note>

***

## Mã lỗi

| HTTP  | `code`                            | Nguyên nhân                                                    |
| ----- | --------------------------------- | -------------------------------------------------------------- |
| `404` | `USER_CODE_NOT_FOUND`             | Không có ai mang mã đó trong tenant                            |
| `409` | `USER_CODE_ALREADY_EXISTS`        | Mã đã thuộc về người khác trong tenant                         |
| `400` | `USER_CODE_DUPLICATED_IN_PAYLOAD` | Một mã xuất hiện nhiều lần trong cùng payload                  |
| `409` | `IDENTITY_IMMUTABLE`              | Cố đổi `email` hoặc `username` của người đã tồn tại            |
| `409` | `USERNAME_ALREADY_EXISTS`         | Tên đăng nhập đã có trên hệ thống                              |
| `409` | `EMAIL_ALREADY_EXISTS`            | Email đã có trên hệ thống                                      |
| `404` | `ORGANIZATION_CODE_NOT_FOUND`     | `organizationCode` không có trong tenant                       |
| `404` | `ROLE_NOT_FOUND_IN_ORGANIZATION`  | Không vai trò nào trong đơn vị đó khớp `roleCode` / `roleName` |
| `400` | `ROLE_AMBIGUOUS_IN_ORGANIZATION`  | Nhiều vai trò trong đơn vị cùng khớp — đổi sang `roleName`     |
| `400` | `MEMBER_ROLE_REQUIRED`            | Một dòng `members` không có cả `roleCode` lẫn `roleName`       |
| `409` | —                                 | Cùng một đơn vị được gán hai vai trò trong `members`           |

Danh sách đầy đủ: [Mã lỗi](/api-reference/ma-loi).
