{
  "openapi": "3.0.4",
  "info": {
    "title": "HRM API",
    "description": "Cổng tích hợp của hệ thống HRM đa quốc gia Việt Nam – Lào.\n\nXác thực bằng khoá API: gửi header Authorization: Bearer hrm_sk_...\nTenant luôn được suy ra từ khoá đã xác thực — không nhận từ header hay body.",
    "version": "v1"
  },
  "paths": {
    "/v1/org/units": {
      "get": {
        "tags": [
          "Cơ cấu tổ chức"
        ],
        "summary": "Danh sách đơn vị tại một ngày",
        "description": "Cần quyền hr.org.unit.view — quyền này được cấp cho mọi vai trò nghiệp vụ. Hai khoá cost_center_code và headcount_quota là dữ liệu NGÂN SÁCH: chúng chỉ xuất hiện trong phản hồi khi người gọi có thêm hr.org.budget.view, và khi không có thì khoá BIẾN MẤT hẳn chứ không trả null. as_of_date bỏ trống thì lấy hôm nay. Kết quả đã sắp theo path nên đọc tuần tự là ra thứ tự cây. Trường parent_id đã được quy về lát cắt cha có hiệu lực tại đúng ngày đang hỏi.",
        "operationId": "org-units-list",
        "parameters": [
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "unit_type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Cơ cấu tổ chức"
        ],
        "summary": "Thêm đơn vị",
        "description": "Cần quyền hr.org.unit.manage. Bắt buộc: code, name_i18n, unit_type, legal_entity_id, valid_from. Cột path do cơ sở dữ liệu tự tính từ cha — truyền tay cũng bị ghi đè, và cây phải được tạo từ trên xuống.",
        "operationId": "org-units-create",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/org/units/tree": {
      "get": {
        "tags": [
          "Cơ cấu tổ chức"
        ],
        "summary": "Cây tổ chức lồng nhau tại một ngày",
        "description": "Cần quyền hr.org.unit.view. Kèm nhân sự thực tế của riêng từng đơn vị (headcount_actual) và của cả nhánh (headcount_subtree). Nhân sự được đếm theo MÃ đơn vị chứ không theo id lát cắt, nên số liệu vẫn đúng sau khi đơn vị được cắt lại. Phần đối chiếu ĐỊNH BIÊN — cost_center_code, headcount_quota, quota_variance, quota_status trên từng nút, cùng quota_total và over_quota ở phần tóm tắt — chỉ có mặt khi người gọi có hr.org.budget.view; không có quyền thì các khoá đó biến mất hẳn.",
        "operationId": "org-units-tree",
        "parameters": [
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/org/units/{id}": {
      "patch": {
        "tags": [
          "Cơ cấu tổ chức"
        ],
        "summary": "Tái cơ cấu: sinh lát cắt mới cho một đơn vị",
        "description": "Cần quyền hr.org.unit.manage. BẮT BUỘC có effective_from — lát cắt hiện tại được đóng vào ngày liền trước, và một lát cắt mới bắt đầu từ ngày đó. Gửi close=true để cho đơn vị ngừng tồn tại kể từ effective_from; thao tác này bị chặn nếu tại ngày đó đơn vị còn nhân sự đang làm việc hoặc còn đơn vị con.",
        "operationId": "org-units-patch",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/formula/functions": {
      "get": {
        "tags": [
          "Công thức"
        ],
        "summary": "Thư viện hàm khả dụng",
        "description": "Dùng để dựng gợi ý tự động trong trình soạn công thức. Cần token đăng nhập hoặc khoá API có phạm vi formula.run.",
        "operationId": "formula-functions",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/formula/compile": {
      "post": {
        "tags": [
          "Công thức"
        ],
        "summary": "Kiểm tra và biên dịch một công thức",
        "description": "Trả về cây AST — đây là thứ được lưu vào cột formula_ast và đánh phiên bản, không phải chuỗi văn bản. Cần token đăng nhập hoặc khoá API có phạm vi formula.run.",
        "operationId": "formula-compile",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EvaluateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/formula/evaluate": {
      "post": {
        "tags": [
          "Công thức"
        ],
        "summary": "Chạy thử một công thức và xem cây giải trình",
        "description": "Mọi con số trên bảng lương phải giải trình được từng bước kèm phiên bản quy tắc đã dùng (nguyên tắc NT-6). Cần token đăng nhập hoặc khoá API có phạm vi formula.run.",
        "operationId": "formula-evaluate",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EvaluateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/formula/sheet": {
      "post": {
        "tags": [
          "Công thức"
        ],
        "summary": "Chạy thử cả một bảng lương nhiều cột",
        "description": "Thứ tự khai báo cột không quan trọng — engine dựng đồ thị phụ thuộc và tự sắp xếp thứ tự tính (quyết định AD-08). Cần token đăng nhập hoặc khoá API có phạm vi formula.run.",
        "operationId": "formula-sheet",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SheetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/events": {
      "get": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Danh mục sự kiện và bộ dữ liệu được hỗ trợ",
        "description": "Đây là hợp đồng công khai với bên tích hợp: thêm mục thì được, đổi tên hoặc bỏ đi sẽ làm hỏng tích hợp đang chạy của khách hàng.",
        "operationId": "integration-events",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/api-keys": {
      "get": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Danh sách khoá API",
        "description": "Phạm vi: integration.keys.read. Không bao giờ trả về giá trị bí mật của khoá — chỉ có tiền tố để nhận diện.",
        "operationId": "api-keys-list",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Tạo khoá API mới",
        "description": "Phạm vi: integration.keys.admin — phạm vi QUẢN TRỊ, '*' KHÔNG bao nó. Khoá mới chỉ được mang những phạm vi mà khoá đang gọi cũng có, và không được sống lâu hơn khoá đó.",
        "operationId": "api-keys-create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/api-keys/{id}": {
      "delete": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Thu hồi một khoá API",
        "description": "Phạm vi: integration.keys.admin khi thu hồi khoá KHÁC. Khoá tự thu hồi chính nó thì không cần phạm vi nào — đó là van an toàn khi khoá bị lộ.",
        "operationId": "api-keys-revoke",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/webhooks": {
      "get": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Danh sách đăng ký webhook",
        "operationId": "webhooks-list",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Đăng ký nhận sự kiện",
        "operationId": "webhooks-create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/webhooks/{id}": {
      "delete": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Huỷ đăng ký webhook",
        "operationId": "webhooks-delete",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/webhooks/publish": {
      "post": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Phát một sự kiện ra các đăng ký đang lắng nghe",
        "description": "Dùng để bên tích hợp thử đường dây trước khi đấu nối thật.",
        "operationId": "webhooks-publish",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/deliveries": {
      "get": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Lịch sử giao sự kiện gần đây",
        "operationId": "webhook-deliveries",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/inbound/{dataset}": {
      "post": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Nhận dữ liệu từ hệ thống bên ngoài",
        "description": "Lưu nguyên bản trước, xử lý sau. Khi bên tích hợp nói 'tôi gửi rồi' thì có bằng chứng để đối chiếu, và xử lý lại được mà không cần họ gửi lại.",
        "operationId": "inbound-receive",
        "parameters": [
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/integration/inbound": {
      "get": {
        "tags": [
          "Cổng tích hợp"
        ],
        "summary": "Các lô dữ liệu đã nhận gần đây",
        "operationId": "inbound-list",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Hệ thống"
        ],
        "summary": "Trạng thái hệ thống",
        "operationId": "health",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/employees": {
      "get": {
        "tags": [
          "Hồ sơ nhân sự"
        ],
        "summary": "Danh sách nhân viên",
        "description": "Cần quyền hr.employee.view. Kết quả đã cắt theo phạm vi dữ liệu của vai trò và đã che theo quyền trường: trường bị ẩn thì KHÔNG còn khoá trong JSON, để bên gọi phân biệt được 'không được xem' với 'chưa có dữ liệu'. as_of_date quyết định lấy lát cắt vị trí công việc nào, mặc định hôm nay.",
        "operationId": "hr-employee-list",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "org_unit_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Hồ sơ nhân sự"
        ],
        "summary": "Tạo hồ sơ nhân viên",
        "description": "Cần quyền hr.employee.create VÀ pháp nhân đích phải nằm trong phạm vi dữ liệu của người gọi: phạm vi ALL tạo được ở mọi pháp nhân, LEGAL_ENTITY chỉ ở pháp nhân của chính mình, các phạm vi hẹp hơn không tạo được ở đâu (403 PERM_OUT_OF_SCOPE). Tạo person, employee và employee_job_info trong MỘT giao dịch — hỏng giữa chừng thì không để lại con người mồ côi không có quan hệ lao động. Mã nhân viên trùng trả về 409. Trùng số căn cước với người đã có chỉ CẢNH BÁO, không chặn, vì rất có thể là người cũ quay lại và cần dùng lại person_id.",
        "operationId": "hr-employee-create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmployeeCreateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/employees/{id}": {
      "get": {
        "tags": [
          "Hồ sơ nhân sự"
        ],
        "summary": "Chi tiết hồ sơ nhân viên",
        "description": "Cần quyền hr.employee.view. Kèm giấy tờ và tài khoản ngân hàng, đều đã che theo quyền trường. Mỗi lần mở hồ sơ sinh đúng MỘT dòng sensitive_access_log cho mỗi cặp (nhóm dữ liệu, mục đích), không phải một dòng mỗi bản ghi: purpose = XEM_DAY_DU khi thật sự thấy giá trị, XEM_DA_CHE khi chỉ mở được hồ sơ còn trường nhạy cảm đã bị che.",
        "operationId": "hr-employee-detail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "patch": {
        "tags": [
          "Hồ sơ nhân sự"
        ],
        "summary": "Sửa hồ sơ nhân viên",
        "description": "Cần quyền hr.employee.update. Đổi đơn vị, chức danh, ngạch, loại hợp đồng hay nơi làm việc thì SINH LÁT CẮT MỚI ở employee_job_info kể từ effective_date và đóng lát cắt cũ — tuyệt đối không ghi đè, vì ghi đè là mất lịch sử theo cách không vá được.",
        "operationId": "hr-employee-update",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmployeeUpdateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/onboarding/checklists": {
      "get": {
        "tags": [
          "Hội nhập"
        ],
        "summary": "Danh sách checklist hội nhập",
        "description": "Cần quyền hr.onboarding.view. PHẠM VI DỮ LIỆU cắt theo NHÂN VIÊN mà checklist nói về — dùng đúng ScopeFilter của tuyến hồ sơ nhân sự, nên nhân viên phạm vi SELF chỉ thấy checklist của chính mình, quản lý thấy của cấp dưới, nhân sự thấy trong pháp nhân mình. Bộ lọc: status (OPEN/DONE/CANCELLED), employee_id, overdue=true để lấy các bản còn việc quá hạn. open_blocking_count là cột DẪN XUẤT do trigger đếm: bản nào còn số dương thì hồ sơ chưa chuyển được từ thử việc sang chính thức.",
        "operationId": "hr-onboarding-checklists-list",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/onboarding/checklists/{id}": {
      "get": {
        "tags": [
          "Hội nhập"
        ],
        "summary": "Chi tiết một checklist hội nhập kèm từng việc",
        "description": "Cần quyền hr.onboarding.view. Trả về từng việc kèm assignee_rule (NGUỒN SỰ THẬT: việc này thuộc về VAI TRÒ nào) và assignee_employee_id (KẾT QUẢ PHÂN GIẢI, có thể null nếu chưa tìm được người). due_date ÂM so với ngày vào làm là chuyện bình thường và có nghĩa: chuẩn bị máy tính, làm thẻ, gửi hợp đồng đi ký đều phải xong TRƯỚC khi người ta đến. Checklist CÓ THẬT nhưng ngoài phạm vi trả 404 giống hệt định danh không tồn tại.",
        "operationId": "hr-onboarding-checklists-detail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/onboarding/checklists/{id}/items/{itemId}:done": {
      "post": {
        "tags": [
          "Hội nhập"
        ],
        "summary": "Đánh dấu một việc hội nhập",
        "description": "Cần quyền hr.onboarding.update. status nhận DONE (mặc định), SKIPPED hoặc NA. SKIPPED bắt buộc skip_reason. Việc có requires_file = true thì KHÔNG đánh DONE được nếu thiếu file_id — một ô tích không có tệp kèm là một ô tích không chứng minh được gì với thanh tra lao động, và cơ sở dữ liệu chặn thêm một lớp bằng ràng buộc ck_checklist_item_can_tep. Đánh dấu một việc CHẶN làm giảm open_blocking_count của bản kích hoạt; khi về 0 thì hồ sơ mới chuyển được sang chính thức.",
        "operationId": "hr-onboarding-items-done",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "itemId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DanhDauViecRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/onboarding/checklists/{id}:close": {
      "post": {
        "tags": [
          "Hội nhập"
        ],
        "summary": "Đóng một checklist hội nhập",
        "description": "Cần quyền hr.onboarding.update. CHỈ đóng được khi không còn việc CHẶN nào đang mở (409 HR_ONBOARD_CON_VIEC_CHAN kèm danh sách việc còn thiếu). Chặn ở CẢ HAI tầng: ở đây để nói được lý do, và ở trigger checklist_instance_kiem_tra để mọi đường ghi khác — nhập liệu tay, tác vụ nền, một endpoint viết sau — cũng bị chặn.",
        "operationId": "hr-onboarding-checklists-close",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/contracts": {
      "get": {
        "tags": [
          "Hợp đồng lao động"
        ],
        "summary": "Danh sách hợp đồng lao động",
        "description": "Cần quyền hr.contract.view. Trả về LÁT CẮT đang hiệu lực tại as_of_date (mặc định hôm nay theo giờ vận hành UTC+7), không phải mọi lát cắt: một hợp đồng có phụ lục giữa kỳ sẽ có nhiều dòng trong bảng nhưng chỉ đúng một dòng có hiệu lực tại mỗi ngày. Kết quả đã cắt theo phạm vi dữ liệu của vai trò và đã che theo quyền trường: trường bị ẩn thì MẤT HẲN KHOÁ trong JSON chứ không trả null, để bên gọi phân biệt được 'không được xem' với 'chưa có dữ liệu'. Mức lương ra dạng { amount, currency, formatted } khi được xem đầy đủ, ra một chuỗi nhãn dải khi vai trò chỉ có mức RANGE.",
        "operationId": "hr-contracts-list",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "employee_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contract_no",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Hợp đồng lao động"
        ],
        "summary": "Soạn hợp đồng lao động mới",
        "description": "Cần quyền hr.contract.create VÀ nhân viên đích phải nằm trong phạm vi dữ liệu của người gọi (403 PERM_OUT_OF_SCOPE + ghi nhật ký REJECT nếu không). Hợp đồng luôn ra đời ở trạng thái DRAFT. Điều khoản được đối chiếu với country pack theo quốc gia của PHÁP NHÂN: vi phạm mức BLOCK trả 422 kèm căn cứ pháp lý, mức WARN vẫn ghi và trả về trong meta.warnings. Quốc gia chưa có country pack thì hệ thống nói thẳng là chưa kiểm được, chứ không im lặng cho qua.",
        "operationId": "hr-contracts-create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContractCreateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/contracts/{id}": {
      "get": {
        "tags": [
          "Hợp đồng lao động"
        ],
        "summary": "Chi tiết một lát cắt hợp đồng",
        "description": "Cần quyền hr.contract.view. Kèm phụ cấp, giấy phụ lục, chữ ký, vết duyệt, tài liệu, và danh sách mọi lát cắt của cùng số hợp đồng. Hợp đồng có thật nhưng nằm ngoài phạm vi dữ liệu của bạn trả về 404 GIỐNG HỆT hợp đồng không tồn tại — 403 sẽ xác nhận rằng định danh này là hợp đồng có thật của một người có thật.",
        "operationId": "hr-contracts-detail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "patch": {
        "tags": [
          "Hợp đồng lao động"
        ],
        "summary": "Ký phụ lục: sinh lát cắt mới cho hợp đồng",
        "description": "Cần quyền hr.contract.update. BẮT BUỘC có effective_from — hệ thống cố ý KHÔNG lấy mặc định hôm nay, vì đoán sai ngày này là sai chi phí lương của cả một kỳ. BẮT BUỘC có khối annex (số phụ lục, ngày ký, lý do): lát cắt từ thứ hai trở đi luôn phải có đúng một giấy phụ lục. Lát cắt hiện tại được ĐÓNG vào ngày liền trước và một lát cắt mới bắt đầu từ effective_from — KHÔNG ghi đè. Mỗi lát cắt là BẢN SAO ĐẦY ĐỦ điều khoản: phụ cấp không gửi lên thì được CHÉP SANG từ lát cắt cũ, gửi lên thì thay thế trọn bộ.",
        "operationId": "hr-contracts-amend",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/contracts/{id}/signatures": {
      "post": {
        "tags": [
          "Hợp đồng lao động"
        ],
        "summary": "Ghi một chữ ký của hợp đồng",
        "description": "Cần quyền hr.contract.update. Chỉ ghi được khi hợp đồng ở trạng thái PENDING_SIGN. Mỗi bên (EMPLOYEE / EMPLOYER) ký đúng một lần — ký lại trả 409 HR_CONTRACT_ALREADY_SIGNED. Bảng chữ ký là CHỈ-GHI-THÊM ở tầng cơ sở dữ liệu: quyền UPDATE và DELETE đã bị thu hồi khỏi vai trò ứng dụng, nên một chữ ký ghi sai chỉ sửa được bằng cách huỷ hợp đồng và soạn lại.",
        "operationId": "hr-contracts-sign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContractSignatureRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/contracts/{id}/approvals": {
      "post": {
        "tags": [
          "Hợp đồng lao động"
        ],
        "summary": "Ghi một quyết định duyệt hợp đồng",
        "description": "Cần quyền hr.contract.approve — CỐ Ý không cấp cho vai trò soạn hợp đồng (HR_SPECIALIST), để người đặt mức lương không đồng thời là người duyệt nó. Mỗi lần trả lại rồi duyệt lại là HAI dòng, không phải một dòng bị sửa.",
        "operationId": "hr-contracts-approve",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContractApprovalRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/contracts/{id}/status": {
      "post": {
        "tags": [
          "Hợp đồng lao động"
        ],
        "summary": "Chuyển trạng thái hợp đồng",
        "description": "Cần quyền hr.contract.update. Máy trạng thái: DRAFT→PENDING_SIGN→ACTIVE→{SUSPENDED↔ACTIVE, EXPIRED, TERMINATED}, DRAFT→CANCELLED. Bước lạ trả 409 HR_CONTRACT_STATUS_INVALID, kiểm ở CẢ tầng endpoint LẪN trigger. Chuyển sang ACTIVE đòi ĐỦ chữ ký hai bên và một quyết định duyệt APPROVED. Chấm dứt hợp đồng KHÔNG đi qua đây — dùng /terminate, vì nó còn phải ghi lý do pháp định và đồng bộ ngày nghỉ việc của hồ sơ.",
        "operationId": "hr-contracts-status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContractStatusRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/hr/contracts/{id}/terminate": {
      "post": {
        "tags": [
          "Hợp đồng lao động"
        ],
        "summary": "Chấm dứt hợp đồng lao động",
        "description": "Cần quyền hr.contract.terminate. Ghi ngày và lý do chấm dứt, ĐÓNG lát cắt đang hiệu lực tại ngày đó, và đồng bộ employee.termination_date. Hồ sơ đã ghi một ngày nghỉ việc KHÁC thì trả 422 HR_CONTRACT_NGAY_NGHI_LECH chứ không ghi đè: hai ngày nghỉ việc khác nhau trên cùng một người là dữ liệu không giải thích được với cơ quan bảo hiểm.",
        "operationId": "hr-contracts-terminate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContractTerminateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/bootstrap/tenant": {
      "post": {
        "tags": [
          "Khởi tạo"
        ],
        "summary": "Tạo tenant và khoá API gốc",
        "description": "Chỉ khả dụng ở môi trường phát triển.",
        "operationId": "bootstrap-tenant",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BootstrapRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/bootstrap/tenants": {
      "get": {
        "tags": [
          "Khởi tạo"
        ],
        "summary": "Danh sách tenant",
        "operationId": "bootstrap-tenants",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/system/audit-log": {
      "get": {
        "tags": [
          "Quản trị hệ thống"
        ],
        "summary": "Nhật ký kiểm toán",
        "description": "Cần quyền system.audit.view. Mọi lần bị từ chối cũng được ghi lại — chuỗi từ chối liên tiếp của một tài khoản là dấu hiệu dò quyền.",
        "operationId": "system-audit-log",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "entity_type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/system/sensitive-access-log": {
      "get": {
        "tags": [
          "Quản trị hệ thống"
        ],
        "summary": "Nhật ký truy cập dữ liệu nhạy cảm",
        "description": "Trả lời câu hỏi kiểm toán luôn hỏi: ai đã xem lương của những ai, lúc nào.",
        "operationId": "system-sensitive-access-log",
        "parameters": [
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/requisitions": {
      "get": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Danh sách đề xuất tuyển dụng",
        "description": "Cần quyền hr.recruit.requisition.view. PHẠM VI DỮ LIỆU: tuyến tuyển dụng cắt theo PHÁP NHÂN của đề xuất. Vai trò có phạm vi ALL thấy tất; vai trò có phạm vi LEGAL_ENTITY thấy các pháp nhân mà chính họ đang có quan hệ lao động; mọi phạm vi HẸP HƠN (SELF, DIRECT_REPORTS, TEAM_TREE, JOB_GRADE_BELOW) KHÔNG thấy đề xuất nào — chúng nói về 'những ai tôi được xem', không nói gì về 'đợt tuyển nào tôi được xem', và suy từ cái thứ nhất ra cái thứ hai luôn suy về phía nới lỏng. as_of_date quyết định lát cắt đơn vị nào được dùng để hiện tên đơn vị và trung tâm chi phí, mặc định hôm nay theo giờ vận hành UTC+7. budget_min/budget_max, headcount_quota_snapshot và headcount_used_snapshot là trường nhạy cảm: bị ẩn thì MẤT HẲN KHOÁ trong JSON chứ không trả null, để bên gọi phân biệt được 'không được xem' với 'chưa có dữ liệu'. Dải ngân sách ra dạng { amount, currency, formatted } khi được xem đầy đủ.",
        "operationId": "hr-recruit-requisitions-list",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "org_unit_code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "campaign_kind",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Soạn đề xuất tuyển dụng mới",
        "description": "Cần quyền hr.recruit.requisition.create VÀ pháp nhân đích phải nằm trong phạm vi dữ liệu của người gọi (403 PERM_OUT_OF_SCOPE kèm một dòng nhật ký REJECT nếu không) — POST không có bản ghi cũ để cắt nên câu hỏi phạm vi được đặt TRƯỚC KHI GHI. Đề xuất luôn ra đời ở trạng thái DRAFT. campaign_kind là LE (tuyển từng vị trí) hoặc HANG_LOAT (tuyển hàng loạt) và nó quyết định nhóm endpoint nào mở ra về sau, nên không sửa được sau khi có hồ sơ ứng tuyển. org_unit_code phải có lát cắt hiệu lực tại ngày kiểm và phải thuộc đúng pháp nhân đó.",
        "operationId": "hr-recruit-requisitions-create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaoDeXuatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/requisitions/{id}": {
      "get": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Chi tiết một đề xuất tuyển dụng",
        "description": "Cần quyền hr.recruit.requisition.view. Đề xuất CÓ THẬT nhưng nằm ngoài phạm vi dữ liệu của bạn trả về 404 GIỐNG HỆT đề xuất không tồn tại — cả mã lỗi lẫn câu thông báo, để không dò được biên phạm vi bằng cách thử định danh.",
        "operationId": "hr-recruit-requisitions-detail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "patch": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Sửa đề xuất tuyển dụng",
        "description": "Cần quyền hr.recruit.requisition.update. CHỈ sửa được khi đề xuất còn ở DRAFT hoặc ON_HOLD: đề xuất đã duyệt mà sửa số lượng hay dải ngân sách là làm quyết định duyệt nói về một thứ khác với thứ đang chạy. Muốn đổi thì đưa về ON_HOLD rồi trình duyệt lại — và lần đó đi qua lại phép kiểm định biên.",
        "operationId": "hr-recruit-requisitions-update",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SuaDeXuatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/requisitions/{id}/approve": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Duyệt đề xuất tuyển dụng",
        "description": "Cần quyền hr.recruit.requisition.approve — CỐ Ý không cấp cho vai trò soạn đề xuất, và cơ sở dữ liệu còn chặn thêm một lớp: approved_by phải khác requested_by. ĐÂY LÀ CHỖ KIỂM ĐỊNH BIÊN: hệ thống cộng (người thực có trong lát cắt đơn vị hiệu lực) + (số còn trống của mọi đề xuất APPROVED cùng đơn vị) + (số lượng của đề xuất này) rồi so với org_unit.headcount_quota, và tra recruitment_policy REQUISITION_OVER_QUOTA theo quốc gia của pháp nhân với scope_key = campaign_kind. BLOCK trả 422 HR_RECRUIT_OVER_HEADCOUNT kèm căn cứ pháp lý và không ghi gì; WARN vẫn duyệt và trả cảnh báo trong meta.warnings. headcount_quota = NULL nghĩa là ĐƠN VỊ CHƯA ĐƯỢC DUYỆT BIÊN CHẾ, không phải 'không giới hạn': hệ thống bỏ qua phép kiểm và cảnh báo HR_RECRUIT_QUOTA_UNKNOWN. Người gọi KHÔNG có hr.org.budget.view nhận cùng mã lỗi nhưng KHÔNG kèm con số nào — in định biên vào thông báo lỗi là để nó đi ra qua một cửa không ai kiểm.",
        "operationId": "hr-recruit-requisitions-approve",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DuyetDeXuatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/requisitions/{id}/status": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Chuyển trạng thái đề xuất tuyển dụng",
        "description": "Cần quyền hr.recruit.requisition.update. Máy trạng thái: DRAFT→{PENDING_APPROVAL, CANCELLED} · PENDING_APPROVAL→{DRAFT, CANCELLED} · APPROVED→{ON_HOLD, CLOSED, CANCELLED} · ON_HOLD→{APPROVED, CLOSED, CANCELLED}. Chuyển sang APPROVED KHÔNG đi qua đây — dùng /approve, vì nó còn phải kiểm định biên và ghi lại người duyệt.",
        "operationId": "hr-recruit-requisitions-status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DoiTrangThaiDeXuatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/requisitions/{id}/candidates:import": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Nhập danh sách ứng viên bằng Excel cho đợt tuyển hàng loạt",
        "description": "Cần quyền hr.recruit.candidate.manage. CHỈ mở cho đề xuất campaign_kind = HANG_LOAT (409 HR_RECRUIT_CHI_HANG_LOAT nếu không). Gửi multipart/form-data với trường 'file' là một tệp .xlsx; dòng đầu là tiêu đề. Cột bắt buộc: full_name, residence_country. Cột tuỳ chọn: candidate_code, full_name_local, email, phone, national_id, national_id_type, date_of_birth, gender, nationality, source_code, referred_by_employee_code. TẤT CẢ HOẶC KHÔNG GÌ CẢ: chỉ cần MỘT dòng hỏng là toàn bộ tệp bị từ chối, không ghi một ứng viên nào, và phản hồi liệt kê TỪNG dòng hỏng kèm SỐ DÒNG trong tệp Excel cùng câu phải sửa gì. Nhập nửa chừng 200 ứng viên là thứ không ai dọn được: không có màn hình xoá hàng loạt, và ứng viên đã ghi thì đã là dữ liệu cá nhân đang lưu. Mỗi ứng viên nhập vào đều được ghi một dòng đồng ý theo văn bản đang phát hành cho quốc gia cư trú của họ, kênh HR_ENTRY — nên tệp phải kèm evidence_file_id, hoặc gửi tham số consent_channel=WEB_FORM nếu danh sách đến từ biểu mẫu ứng tuyển do chính ứng viên điền.",
        "operationId": "hr-recruit-requisitions-import",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NhapUngVienRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/candidates": {
      "get": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Danh sách ứng viên",
        "description": "Cần quyền hr.recruit.candidate.view. PHẠM VI: cắt theo candidate.owner_legal_entity_id, cùng luật với đề xuất — phạm vi hẹp hơn LEGAL_ENTITY không thấy ứng viên nào. MỖI LẦN ĐỌC SINH MỘT DÒNG sensitive_access_log, kể cả khi mọi trường nhạy cảm đều bị che: nếu chỉ ghi khi dữ liệu thật sự lộ ra thì câu 'ai đã mở hồ sơ của tôi' — một quyền của chủ thể dữ liệu — mất câu trả lời cho gần như mọi vai trò. Dòng ghi khi lộ mang purpose XEM_DAY_DU, dòng ghi khi bị che mang XEM_DA_CHE; ngưỡng cảnh báo 'xem quá nhiều hồ sơ mỗi giờ' phải lọc XEM_DAY_DU, nếu không thì mỗi lần mở màn hình đều bị đếm. Số căn cước, thư điện tử, số điện thoại và ngày sinh mặc định ĐÓNG; parsed_data và cv_file_id ẩn hẳn — định danh tệp CV chính là chìa khoá tải tệp nên nó không bao giờ đi kèm danh sách. Sắp xếp chỉ nhận candidate_code, full_name, status, created_at: cho sắp theo một cột đang bị che là để suy ra thứ tự của cả danh sách mà không cần đọc.",
        "operationId": "hr-recruit-candidates-list",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "legal_entity_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "residence_country",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source_code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chua_dong_y",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "qua_han_luu",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Tạo hồ sơ ứng viên",
        "description": "Cần quyền hr.recruit.candidate.manage và pháp nhân sở hữu phải nằm trong phạm vi dữ liệu của người gọi. residence_country là QUỐC GIA CƯ TRÚ và BẮT BUỘC khai tay, KHÔNG suy từ pháp nhân: một pháp nhân Việt Nam tuyển kỹ sư đang cư trú ở Lào thì người đó thuộc khung bảo vệ dữ liệu của nơi họ ở, và đưa cho họ một văn bản viết cho hệ thống pháp luật khác là đưa một văn bản không có giá trị chứng minh. Hệ thống chọn consent_document theo (quốc gia cư trú, mục đích, ngày), ưu tiên bản riêng của khách hàng rồi tới phiên bản mới nhất; KHÔNG có bản nào thì 422 HR_RECRUIT_CONSENT_DOC_MISSING và KHÔNG lưu hồ sơ. Đây là chỗ DUY NHẤT của phân hệ mà thiếu dữ liệu pack thì CHẶN chứ không cảnh báo, vì thiếu văn bản đồng ý nghĩa là không có cơ sở pháp lý để xử lý dữ liệu. Gửi kèm khối consent để ghi nhận đồng ý ngay trong cùng giao dịch; kênh PAPER và HR_ENTRY bắt buộc có evidence_file_id vì đồng ý do người khác nhập hộ mà không có bằng chứng thì không phải đồng ý, chỉ là một dòng dữ liệu.",
        "operationId": "hr-recruit-candidates-create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaoUngVienRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/candidates/{id}": {
      "get": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Chi tiết một ứng viên",
        "description": "Cần quyền hr.recruit.candidate.view. Kèm lịch sử đồng ý và rút đồng ý, và danh sách hồ sơ ứng tuyển của người đó. Ứng viên CÓ THẬT nhưng ngoài phạm vi trả 404 giống hệt định danh không tồn tại — với ứng viên, một câu 403 đã là tiết lộ rằng người này có hồ sơ trong kho CV của công ty.",
        "operationId": "hr-recruit-candidates-detail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "patch": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Sửa hồ sơ ứng viên",
        "description": "Cần quyền hr.recruit.candidate.manage. Hồ sơ đã ẩn danh KHÔNG sửa được (422 HR_RECRUIT_CANDIDATE_ANONYMIZED) — ghi dữ liệu định danh trở lại một hồ sơ đã ẩn danh là huỷ chính việc ẩn danh đó. Bốn cột dẫn xuất consent_at, consent_withdrawn_at, retention_until, birth_year do trigger tính, gửi lên cũng bị bỏ qua.",
        "operationId": "hr-recruit-candidates-update",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SuaUngVienRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/candidates/{id}/consents": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Ghi nhận một lần đồng ý của ứng viên",
        "description": "Cần quyền hr.recruit.candidate.manage. Bảng đồng ý là CHỈ-GHI-THÊM ở tầng cơ sở dữ liệu (quyền UPDATE và DELETE đã bị thu hồi khỏi vai trò ứng dụng), nên rút lại đồng ý là một DÒNG MỚI chứ không phải một dòng bị sửa: sửa dòng cũ là xoá bằng chứng rằng người ta TỪNG đồng ý, mà chính bằng chứng đó chứng minh việc thu thập trước đó là hợp pháp. Hệ thống chép cứng phiên bản và mã băm nội dung văn bản vào dòng đồng ý, để về sau còn trả lời được 'họ đã đồng ý với nội dung nào'. Thời hạn lưu áp cho ứng viên là con số ĐÃ HỨA trong chính văn bản họ ký, không phải con số trong chính sách hiện hành — đọc theo chính sách hiện hành là âm thầm kéo dài thời hạn lưu của những người đã đồng ý với một con số khác.",
        "operationId": "hr-recruit-candidates-consent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DongYRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/candidates/{id}/consents:withdraw": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Ghi nhận ứng viên rút lại sự đồng ý",
        "description": "Cần quyền hr.recruit.candidate.manage. Bốn hệ quả, không phải một: (1) thời hạn lưu rút ngắn về ngày rút cộng số ngày ân hạn của recruitment_policy ANONYMIZE_GRACE_DAYS — thiếu quy tắc thì lấy ĐÚNG ngày rút và cảnh báo, tức nghiêng về phía ứng viên chứ không nghiêng về phía công ty; (2) mọi hồ sơ ứng tuyển đang mở của người đó chuyển sang giai đoạn WITHDRAWN với withdrawn_by = CANDIDATE; (3) KHÔNG xoá ngay, vì phải giữ được bằng chứng đã xử lý đúng yêu cầu và vì một hồ sơ đang giữa vòng phỏng vấn thì phải khoá lại có thông báo chứ không bốc hơi khỏi lịch của ngày mai; (4) ứng viên ĐÃ trở thành nhân viên thì rút đồng ý tuyển dụng KHÔNG kéo theo xoá hồ sơ nhân sự — cơ sở pháp lý đã chuyển sang thực hiện hợp đồng lao động và nghĩa vụ pháp lý.",
        "operationId": "hr-recruit-candidates-consent-withdraw",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RutDongYRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/candidates/{id}:anonymize": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Ẩn danh hồ sơ ứng viên",
        "description": "Cần quyền RIÊNG hr.recruit.privacy.execute — không mượn hr.recruit.candidate.manage, vì đây là thao tác KHÔNG cuộn ngược được. ẨN DANH CHỨ KHÔNG XOÁ CỨNG: xoá cứng làm mất báo cáo tuyển dụng nhiều năm, mà sáu câu hỏi của báo cáo đó (nguồn nào hiệu quả, thời gian tuyển trung bình, tỷ lệ rơi theo giai đoạn, lý do từ chối, cơ cấu giới và tuổi, mức đề nghị trung bình) chỉ cần MÃ và MỐC THỜI GIAN. Xoá: họ tên, thư điện tử, điện thoại, số giấy tờ, ngày sinh, parsed_data, định danh tệp CV, ghi chú từ chối, nhận xét phỏng vấn. Giữ: mã, quốc tịch, quốc gia cư trú, giới tính, NĂM sinh, nguồn, mọi mốc thời gian, điểm và khuyến nghị phỏng vấn, và toàn bộ nhật ký đồng ý (đó là bằng chứng, không phải dữ liệu định danh). Sinh một biên bản candidate_anonymization_log ghi TÊN TRƯỜNG đã xoá và TUYỆT ĐỐI KHÔNG ghi giá trị cũ — biên bản chứa giá trị cũ là ẩn danh xong mà dữ liệu vẫn còn nguyên ở một bảng chỉ-ghi-thêm không xoá được.",
        "operationId": "hr-recruit-candidates-anonymize",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AnDanhRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications": {
      "get": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Danh sách hồ sơ ứng tuyển",
        "description": "Cần quyền hr.recruit.application.view. Cắt phạm vi GIÁN TIẾP qua pháp nhân của đề xuất. Tên giai đoạn phân giải theo as_of_date chứ không theo hôm nay: giai đoạn đổi tên thì màn hình xem lại kỳ cũ phải in tên cũ. stage_kind là NGỮ NGHĨA ổn định của giai đoạn (NEW, SCREENING, INTERVIEW, ASSESSMENT, OFFER, HIRED, REJECTED, WITHDRAWN) — hãy dựng logic trên nó, đừng dựng trên stage_code vì mã và tên là thứ khách hàng sửa được. offer_amount, rating và rejection_note mặc định ĐÓNG.",
        "operationId": "hr-recruit-applications-list",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "size",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "requisition_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "candidate_id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "stage_code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "stage_kind",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Nộp hồ sơ ứng tuyển vào một đề xuất",
        "description": "Cần quyền hr.recruit.application.manage. Đề xuất phải ở trạng thái APPROVED. Hồ sơ vào thẳng giai đoạn is_default_entry của tenant; tenant CHƯA gieo giai đoạn nào thì 422 HR_RECRUIT_PIPELINE_MISSING chứ hệ thống KHÔNG tự sinh ngầm một pipeline mặc định — sinh ngầm là khách hàng có một quy trình họ chưa từng đồng ý và không ai biết nó từ đâu ra. Một ứng viên chỉ nộp được MỘT lần vào MỘT đề xuất (409 HR_RECRUIT_HO_SO_TRUNG); nộp vào nhiều đề xuất khác nhau thì được. Ứng viên của pháp nhân A nộp vào đề xuất của pháp nhân B chỉ đi được khi recruitment_policy CROSS_ENTITY_TALENT_POOL cho phép — mặc định chặn, vì chia sẻ hồ sơ giữa pháp nhân Việt Nam và pháp nhân Lào LÀ chuyển dữ liệu cá nhân xuyên biên giới.",
        "operationId": "hr-recruit-applications-create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaoHoSoRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications/{id}": {
      "get": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Chi tiết một hồ sơ ứng tuyển",
        "description": "Cần quyền hr.recruit.application.view. Kèm lịch sử chuyển giai đoạn, lịch phỏng vấn và nhận xét từng vòng. Hồ sơ có thật ngoài phạm vi trả 404.",
        "operationId": "hr-recruit-applications-detail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "as_of_date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications/{id}/stage": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Chuyển hồ sơ sang giai đoạn khác",
        "description": "Cần quyền hr.recruit.application.manage, cộng thêm mã quyền ghi trong application_stage_transition.requires_permission nếu tenant có khai cặp chuyển tiếp đó. Tenant KHÔNG khai dòng nào trong bảng chuyển tiếp thì dùng luật mặc định của mã: tiến tới sort_order lớn hơn được, lùi thì bắt buộc kèm lý do. Khai MỘT dòng là bật chế độ 'chỉ những cặp đã khai mới đi được', nên gieo nửa vời còn tệ hơn để rỗng. Vào REJECTED bắt buộc rejection_reason_code còn hiệu lực — lý do gõ tự do là bốn cách gõ thành bốn nhóm thống kê và cả báo cáo phễu mất nghĩa. Vào OFFER bắt buộc có đủ mức đề nghị, đồng tiền và ngày hết hạn. Vào HIRED thì đợt LE bắt buộc offer_status = ACCEPTED, còn đợt HANG_LOAT tra chính sách MASS_HIRE_REQUIRES_OFFER. Ba giai đoạn HIRED, REJECTED, WITHDRAWN là ĐÍCH CUỐI: ra khỏi chúng cần quyền hr.recruit.reopen và bắt buộc ghi lý do, và hồ sơ đã sinh ra một nhân viên thì KHÔNG mở lại được bằng bất kỳ quyền nào. Mỗi lần chuyển sinh một dòng application_stage_history do TRIGGER ghi — tầng ứng dụng đặt app.identity_id trong cùng giao dịch để lịch sử trả lời được cả câu 'ai đổi'.",
        "operationId": "hr-recruit-applications-stage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChuyenGiaiDoanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications/{id}/offer": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Soạn hoặc sửa phiếu đề nghị",
        "description": "Cần quyền hr.recruit.offer.manage — TÁCH khỏi quyền sửa hồ sơ vì đây là TIỀN. Mức đề nghị nằm ngoài dải [budget_min, budget_max] của đề xuất thì tra recruitment_policy OFFER_OUT_OF_BUDGET: BLOCK trả 422, WARN vẫn ghi và cảnh báo, thiếu quy tắc thì cảnh báo HR_RECRUIT_POLICY_RULE_MISSING chứ KHÔNG đoán một con số. Tuổi ứng viên tại offer_start_date được đối chiếu với MIN_WORKING_AGE ngay ở bước này chứ không đợi lúc tạo hồ sơ nhân viên: phát hiện ở bước cuối là đã hứa với một người mà công ty không tuyển được. Ứng viên mang quốc tịch khác quốc gia của pháp nhân thì tra WORK_PERMIT_REQUIRED_AT để cảnh báo về giấy phép lao động. ⚠ QUYỀN GHI VÀ QUYỀN ĐỌC LÀ HAI QUYẾT ĐỊNH TÁCH RỜI: offer_amount nằm trong nhóm bí danh lương, nên một vai trò ghi được mức đề nghị vẫn có thể KHÔNG đọc lại được nó trong phản hồi. Đó là hành vi đúng, không phải lỗi.",
        "operationId": "hr-recruit-applications-offer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SoanDeNghiRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications/{id}/offer:approve": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Duyệt phiếu đề nghị",
        "description": "Cần quyền hr.recruit.offer.approve — CỐ Ý không cấp cho vai trò giữ hr.recruit.offer.manage, để người đặt mức lương không đồng thời là người duyệt nó. Chỉ duyệt được phiếu đang ở DRAFT hoặc PENDING_APPROVAL.",
        "operationId": "hr-recruit-applications-offer-approve",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DuyetDeNghiRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications/{id}/interviews": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Lên lịch một vòng phỏng vấn",
        "description": "Cần quyền hr.recruit.application.manage. Mỗi vòng (round_no) chỉ có một lịch; gửi lại cùng số vòng trả 409. interview_type_code tra danh mục interview_type theo lát cắt hiệu lực tại ngày phỏng vấn.",
        "operationId": "hr-recruit-applications-interview",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LenLichPhongVanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications/{id}/interviews/{interviewId}/status": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Đóng một buổi phỏng vấn: DONE, CANCELLED hoặc NO_SHOW",
        "description": "Cần quyền hr.recruit.application.manage. Tuyến này là ĐIỀU KIỆN để dùng được các giai đoạn khai requires_interview: cờ đó được cưỡng chế bằng câu hỏi 'hồ sơ đã có buổi phỏng vấn nào DONE chưa', nên khi không có đường đặt DONE thì những giai đoạn ấy KHÔNG BAO GIỜ vào được — và hỏng trong im lặng, vì luật tiến mặc định vẫn cho nhảy qua chúng để đi thẳng tới giai đoạn đề nghị. Chỉ buổi đang SCHEDULED mới đóng được; đóng lại lần nữa trả 409 kèm trạng thái hiện tại. Trạng thái đích KHÔNG nhận SCHEDULED: quay ngược một buổi đã kết thúc về 'đã lên lịch' là xoá dấu vết nó từng diễn ra, trong khi các phiếu chấm của buổi đó vẫn nằm nguyên ở bảng chỉ-ghi-thêm — cần gặp lại thì lên lịch một VÒNG MỚI. CANCELLED và NO_SHOW bắt buộc kèm note: cả hai đếm vào phễu tuyển dụng, và một con số rơi không kèm lý do là con số không ai giải thích được ở kỳ đánh giá. Bản ghi ngoài phạm vi trả 404 giống hệt định danh không tồn tại.",
        "operationId": "hr-recruit-applications-interview-status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "interviewId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DoiTrangThaiPhongVanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications/{id}/interviews/{interviewId}/feedback": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Ghi nhận xét của một người phỏng vấn",
        "description": "Cần quyền hr.recruit.application.manage. Bảng nhận xét là CHỈ-GHI-THÊM ở tầng cơ sở dữ liệu: sửa một nhận xét SAU KHI ứng viên bị loại là xoá bằng chứng của một quyết định tuyển dụng. Người phỏng vấn đổi ý thì chèn nhận xét ở vòng mới, để cả hai ý kiến còn nguyên. Mỗi người chấm đúng một lần cho mỗi vòng (409 nếu lặp). Điểm, khuyến nghị và nhận xét là phán xét về một CON NGƯỜI nên mặc định ĐÓNG, và chúng không có mức lưng chừng: hoặc đọc được, hoặc mất hẳn khoá.",
        "operationId": "hr-recruit-applications-feedback",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "interviewId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChamPhongVanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications/{id}:convert": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Chuyển ứng viên trúng tuyển thành nhân viên",
        "description": "Cần ĐỦ CẢ BA quyền hr.recruit.convert, hr.employee.create và hr.contract.create — phép GIAO, không phải một quyền thay thế hai quyền kia: luồng này sinh ra một con người, một hồ sơ nhân viên và một hợp đồng có mức lương, nên nó phải đi qua đúng những cổng đang canh ba thứ đó. MỘT GIAO DỊCH DUY NHẤT cho cả mười bước: person, giấy tờ và CV, employee, lát cắt vị trí, hợp đồng NHÁP, quyết định tuyển (movement HIRE), nối hợp đồng với quyết định, checklist hội nhập, đóng hồ sơ ứng tuyển, nối ứng viên sang hồ sơ nhân sự. Hỏng bất kỳ bước nào thì KHÔNG còn dấu vết nào — chuyển đổi nửa chừng là hỏng dữ liệu mà người dùng không sửa được từ giao diện. Hợp đồng đi qua ĐÚNG đường tạo hợp đồng của phân hệ hợp đồng, nên nó chịu đủ cổng phạm vi, kiểm loại hợp đồng theo quốc gia và ngày, chống trùng số hợp đồng, và đối chiếu country pack; vi phạm mức BLOCK cuộn ngược CẢ CỤM. ĐỐI CHIẾU TRÙNG LẶP trả 409 kèm danh sách person_id để NGƯỜI DÙNG quyết, hệ thống không bao giờ tự gộp: trả lời bằng reuse_person_id (dùng lại, giữ nguyên thâm niên) hoặc confirm_new_person = true (người khác). Người đang là nhân viên của chính pháp nhân đó trả 409 — nghiệp vụ thật thường là thuyên chuyển. Người đã nghỉ ở đó là TÁI TUYỂN DỤNG: employment mới, mã nhân viên mới, person cũ. Bốn ngày phải bằng nhau sau khi chạy: employee.hire_date, movement effective_date, checklist anchor_date và contract start_date, tất cả lấy từ application.offer_start_date. Thời hạn lưu dữ liệu ứng viên ĐỔI ĐỒNG HỒ: candidate.retention_until về NULL vì cơ sở pháp lý chuyển từ SỰ ĐỒNG Ý sang thực hiện hợp đồng lao động, và từ đây đồng hồ của NHÂN VIÊN (legal_entity.retention_years tính từ ngày nghỉ) chi phối. Hồ sơ CÓ THẬT nhưng ngoài phạm vi trả 404 giống hệt định danh không tồn tại. Phản hồi KHÔNG mang một con số tiền nào.",
        "operationId": "hr-recruit-applications-convert",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChuyenDoiRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/recruit/applications:bulk-convert": {
      "post": {
        "tags": [
          "Tuyển dụng"
        ],
        "summary": "Chuyển cả nhóm ứng viên trúng tuyển thành nhân viên",
        "description": "Cần cùng bộ ba quyền như tuyến lẻ, và chạy CÙNG MỘT luật cho từng dòng — đây là cùng một hàm lõi gọi trong vòng lặp, không phải một đường thứ hai. TẤT CẢ HOẶC KHÔNG GÌ CẢ: cả lô nằm trong MỘT giao dịch, nên một dòng hỏng cuộn ngược mọi dòng đã chạy trước đó. Đổi lại không bao giờ có trạng thái nửa vời kiểu 'một trăm hai mươi người đã thành nhân viên, tám mươi người thì chưa' — trạng thái mà không màn hình nào hiện ra được và không ai dọn được, vì mỗi người đã chuyển đổi kéo theo một person, một hợp đồng, một quyết định tuyển và một checklist. Lỗi vì thế luôn chỉ ĐÚNG dòng hỏng, kèm số thứ tự dòng và application_id. Trần 200 hồ sơ mỗi lượt: một giao dịch dài giữ khoá trên bảng nhân viên suốt thời gian đó, nên lô lớn hơn phải chia nhỏ — mỗi lô vẫn nguyên vẹn ngữ nghĩa, chỉ là ranh giới nằm ở chỗ người vận hành chọn. Dữ kiện dùng chung (loại hợp đồng, ngày ký, loại lương, kỳ trả) khai MỘT LẦN ở khối chung; mỗi dòng chỉ khai phần riêng. Trùng application_id trong cùng lô bị từ chối TRƯỚC khi ghi.",
        "operationId": "hr-recruit-applications-bulk-convert",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChuyenDoiHangLoatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/auth/login": {
      "post": {
        "tags": [
          "Xác thực"
        ],
        "summary": "Đăng nhập",
        "description": "Trả về access token sống ngắn và refresh token sống dài. Sai mật khẩu 5 lần liên tiếp thì tài khoản bị khoá 15 phút.",
        "operationId": "auth-login",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/auth/refresh": {
      "post": {
        "tags": [
          "Xác thực"
        ],
        "summary": "Làm mới phiên",
        "description": "Token cũ bị thu hồi ngay khi đổi. Dùng lại token đã đổi sẽ bị từ chối — đây là dấu hiệu token bị đánh cắp.",
        "operationId": "auth-refresh",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/auth/logout": {
      "post": {
        "tags": [
          "Xác thực"
        ],
        "summary": "Đăng xuất",
        "operationId": "auth-logout",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/auth/me": {
      "get": {
        "tags": [
          "Xác thực"
        ],
        "summary": "Thông tin phiên hiện tại",
        "description": "Trả về đủ ba chiều quyền: chức năng, phạm vi dữ liệu, và quyền trường. Giao diện dựa vào đây để ẩn hiện chức năng, nhưng máy chủ vẫn kiểm lại ở từng lời gọi.",
        "operationId": "auth-me",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/auth/change-password": {
      "post": {
        "tags": [
          "Xác thực"
        ],
        "summary": "Đổi mật khẩu",
        "operationId": "auth-change-password",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangePasswordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/auth/bootstrap-admin": {
      "post": {
        "tags": [
          "Xác thực"
        ],
        "summary": "Tạo tài khoản quản trị đầu tiên",
        "description": "Chỉ dùng được ở môi trường phát triển và chỉ khi tổ chức chưa có tài khoản nào.",
        "operationId": "auth-bootstrap-admin",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BootstrapAdminRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AnDanhRequest": {
        "type": "object",
        "properties": {
          "triggerReason": {
            "type": "string",
            "nullable": true
          },
          "legalBasis": {
            "type": "string",
            "nullable": true
          },
          "batchId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "BootstrapAdminRequest": {
        "type": "object",
        "properties": {
          "tenantCode": {
            "type": "string",
            "nullable": true
          },
          "username": {
            "type": "string",
            "nullable": true
          },
          "password": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "BootstrapRequest": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "plan": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ChamPhongVanRequest": {
        "type": "object",
        "properties": {
          "interviewerEmployeeId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "score": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "recommendation": {
            "type": "string",
            "nullable": true
          },
          "comment": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ChangePasswordRequest": {
        "type": "object",
        "properties": {
          "currentPassword": {
            "type": "string",
            "nullable": true
          },
          "newPassword": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ChuyenDoiChungRequest": {
        "type": "object",
        "properties": {
          "contractTypeCode": {
            "type": "string",
            "nullable": true
          },
          "contractSignDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "contractEndDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "salaryType": {
            "type": "string",
            "nullable": true
          },
          "payFrequency": {
            "type": "string",
            "nullable": true
          },
          "salary": {
            "$ref": "#/components/schemas/ContractMoneyInput"
          },
          "managerEmployeeId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "workLocation": {
            "type": "string",
            "nullable": true
          },
          "checklistTemplateId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ChuyenDoiDongRequest": {
        "type": "object",
        "properties": {
          "applicationId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "reusePersonId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "confirmNewPerson": {
            "type": "boolean"
          },
          "employeeCode": {
            "type": "string",
            "nullable": true
          },
          "contractNo": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ChuyenDoiHangLoatRequest": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChuyenDoiDongRequest"
            },
            "nullable": true
          },
          "chung": {
            "$ref": "#/components/schemas/ChuyenDoiChungRequest"
          }
        },
        "additionalProperties": false
      },
      "ChuyenDoiRequest": {
        "type": "object",
        "properties": {
          "reusePersonId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "confirmNewPerson": {
            "type": "boolean"
          },
          "employeeCode": {
            "type": "string",
            "nullable": true
          },
          "contractNo": {
            "type": "string",
            "nullable": true
          },
          "contractTypeCode": {
            "type": "string",
            "nullable": true
          },
          "contractSignDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "contractEndDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "salaryType": {
            "type": "string",
            "nullable": true
          },
          "payFrequency": {
            "type": "string",
            "nullable": true
          },
          "salary": {
            "$ref": "#/components/schemas/ContractMoneyInput"
          },
          "managerEmployeeId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "workLocation": {
            "type": "string",
            "nullable": true
          },
          "checklistTemplateId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ChuyenGiaiDoanRequest": {
        "type": "object",
        "properties": {
          "stageCode": {
            "type": "string",
            "nullable": true
          },
          "reasonCode": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "withdrawnBy": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ContractAllowanceInput": {
        "type": "object",
        "properties": {
          "payComponentCode": {
            "type": "string",
            "nullable": true
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "percent": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ContractApprovalRequest": {
        "type": "object",
        "properties": {
          "decision": {
            "type": "string",
            "nullable": true
          },
          "comment": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ContractCreateRequest": {
        "type": "object",
        "properties": {
          "employeeId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "employeeCode": {
            "type": "string",
            "nullable": true
          },
          "contractNo": {
            "type": "string",
            "nullable": true
          },
          "contractTypeCode": {
            "type": "string",
            "nullable": true
          },
          "signDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "startDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "endDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "jobTitle": {
            "type": "string",
            "nullable": true
          },
          "jobGrade": {
            "type": "string",
            "nullable": true
          },
          "positionCode": {
            "type": "string",
            "nullable": true
          },
          "orgUnitCode": {
            "type": "string",
            "nullable": true
          },
          "managerEmployeeId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "workLocationCode": {
            "type": "string",
            "nullable": true
          },
          "workShiftCode": {
            "type": "string",
            "nullable": true
          },
          "weeklyHours": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "probationFrom": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "probationTo": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "probationRatePct": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "probationSalary": {
            "$ref": "#/components/schemas/ContractMoneyInput"
          },
          "probationGroup": {
            "type": "string",
            "nullable": true
          },
          "salaryType": {
            "type": "string",
            "nullable": true
          },
          "salary": {
            "$ref": "#/components/schemas/ContractMoneyInput"
          },
          "payFrequency": {
            "type": "string",
            "nullable": true
          },
          "siBase": {
            "$ref": "#/components/schemas/ContractMoneyInput"
          },
          "renewedFromContractNo": {
            "type": "string",
            "nullable": true
          },
          "allowances": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ContractAllowanceInput"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ContractMoneyInput": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ContractSignatureRequest": {
        "type": "object",
        "properties": {
          "party": {
            "type": "string",
            "nullable": true
          },
          "method": {
            "type": "string",
            "nullable": true
          },
          "signerName": {
            "type": "string",
            "nullable": true
          },
          "signerTitle": {
            "type": "string",
            "nullable": true
          },
          "certificateSerial": {
            "type": "string",
            "nullable": true
          },
          "certificateIssuer": {
            "type": "string",
            "nullable": true
          },
          "fileId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ContractStatusRequest": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ContractTerminateRequest": {
        "type": "object",
        "properties": {
          "terminatedOn": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "terminationReasonCode": {
            "type": "string",
            "nullable": true
          },
          "noticeGivenOn": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "lastWorkingDay": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "severance": {
            "$ref": "#/components/schemas/ContractMoneyInput"
          },
          "reason": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "CreateApiKeyRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "CreateWebhookRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "targetUrl": {
            "type": "string",
            "nullable": true
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "DanhDauViecRequest": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "nullable": true
          },
          "fileId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "skipReason": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "DoiTrangThaiDeXuatRequest": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "DoiTrangThaiPhongVanRequest": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "DongYRequest": {
        "type": "object",
        "properties": {
          "purpose": {
            "type": "string",
            "nullable": true
          },
          "channel": {
            "type": "string",
            "nullable": true
          },
          "evidenceFileId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "grantedIp": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "DuyetDeNghiRequest": {
        "type": "object",
        "properties": {
          "approvedBy": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "DuyetDeXuatRequest": {
        "type": "object",
        "properties": {
          "approvedBy": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "effectiveDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "EmployeeCreateRequest": {
        "type": "object",
        "properties": {
          "person": {
            "$ref": "#/components/schemas/EmployeePersonInput"
          },
          "employeeCode": {
            "type": "string",
            "nullable": true
          },
          "legalEntityId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "legalEntityCode": {
            "type": "string",
            "nullable": true
          },
          "hireDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "job": {
            "$ref": "#/components/schemas/EmployeeJobInput"
          },
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EmployeeDocumentInput"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "EmployeeDocumentInput": {
        "type": "object",
        "properties": {
          "docType": {
            "type": "string",
            "nullable": true
          },
          "docNumber": {
            "type": "string",
            "nullable": true
          },
          "issuedDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "expiryDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "issuedPlace": {
            "type": "string",
            "nullable": true
          },
          "country": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "EmployeeJobInput": {
        "type": "object",
        "properties": {
          "orgUnitId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "orgUnitCode": {
            "type": "string",
            "nullable": true
          },
          "jobTitle": {
            "type": "string",
            "nullable": true
          },
          "jobGrade": {
            "type": "string",
            "nullable": true
          },
          "employmentType": {
            "type": "string",
            "nullable": true
          },
          "managerEmployeeId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "workLocation": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "EmployeePersonInput": {
        "type": "object",
        "properties": {
          "personId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "fullName": {
            "type": "string",
            "nullable": true
          },
          "fullNameLocal": {
            "type": "string",
            "nullable": true
          },
          "dateOfBirth": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "gender": {
            "type": "string",
            "nullable": true
          },
          "nationality": {
            "type": "string",
            "nullable": true
          },
          "nationalId": {
            "type": "string",
            "nullable": true
          },
          "nationalIdType": {
            "type": "string",
            "nullable": true
          },
          "personalEmail": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "EmployeeUpdateRequest": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "effectiveDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "nullable": true
          },
          "person": {
            "$ref": "#/components/schemas/EmployeePersonInput"
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "terminationDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "job": {
            "$ref": "#/components/schemas/EmployeeJobInput"
          }
        },
        "additionalProperties": false
      },
      "EvaluateRequest": {
        "type": "object",
        "properties": {
          "formula": {
            "type": "string",
            "nullable": true
          },
          "values": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/JsonNode"
            },
            "nullable": true
          },
          "currency": {
            "type": "string",
            "nullable": true
          },
          "trace": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "JsonNode": {
        "type": "object",
        "properties": {
          "options": {
            "$ref": "#/components/schemas/JsonNodeOptions"
          },
          "parent": {
            "$ref": "#/components/schemas/JsonNode"
          },
          "root": {
            "$ref": "#/components/schemas/JsonNode"
          }
        },
        "additionalProperties": false
      },
      "JsonNodeOptions": {
        "type": "object",
        "properties": {
          "propertyNameCaseInsensitive": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "LenLichPhongVanRequest": {
        "type": "object",
        "properties": {
          "roundNo": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "interviewTypeCode": {
            "type": "string",
            "nullable": true
          },
          "scheduledAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "durationMinutes": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "meetingUrl": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "LoginRequest": {
        "type": "object",
        "properties": {
          "tenantCode": {
            "type": "string",
            "nullable": true
          },
          "username": {
            "type": "string",
            "nullable": true
          },
          "password": {
            "type": "string",
            "nullable": true
          },
          "device": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "NhapUngVienRequest": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TaoUngVienRequest"
            },
            "nullable": true
          },
          "consentChannel": {
            "type": "string",
            "nullable": true
          },
          "evidenceFileId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "RefreshRequest": {
        "type": "object",
        "properties": {
          "refreshToken": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "RutDongYRequest": {
        "type": "object",
        "properties": {
          "channel": {
            "type": "string",
            "nullable": true
          },
          "evidenceFileId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SheetComponentRequest": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "nullable": true
          },
          "formula": {
            "type": "string",
            "nullable": true
          },
          "roundingScale": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false
      },
      "SheetRequest": {
        "type": "object",
        "properties": {
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SheetComponentRequest"
            },
            "nullable": true
          },
          "values": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/JsonNode"
            },
            "nullable": true
          },
          "currency": {
            "type": "string",
            "nullable": true
          },
          "trace": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SoanDeNghiRequest": {
        "type": "object",
        "properties": {
          "offerAmount": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "offerCurrency": {
            "type": "string",
            "nullable": true
          },
          "offerExpiresOn": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "offerStartDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "offerStatus": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SuaDeXuatRequest": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "orgUnitCode": {
            "type": "string",
            "nullable": true
          },
          "jobTitle": {
            "type": "string",
            "nullable": true
          },
          "jobGrade": {
            "type": "string",
            "nullable": true
          },
          "employmentType": {
            "type": "string",
            "nullable": true
          },
          "headcount": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "reasonCode": {
            "type": "string",
            "nullable": true
          },
          "defaultContractTypeCode": {
            "type": "string",
            "nullable": true
          },
          "budgetMin": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "budgetMax": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "budgetCurrency": {
            "type": "string",
            "nullable": true
          },
          "targetDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SuaUngVienRequest": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "fullName": {
            "type": "string",
            "nullable": true
          },
          "fullNameLocal": {
            "type": "string",
            "nullable": true
          },
          "dateOfBirth": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "gender": {
            "type": "string",
            "nullable": true
          },
          "nationality": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "nationalId": {
            "type": "string",
            "nullable": true
          },
          "nationalIdType": {
            "type": "string",
            "nullable": true
          },
          "sourceCode": {
            "type": "string",
            "nullable": true
          },
          "cvFileId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "TaoDeXuatRequest": {
        "type": "object",
        "properties": {
          "legalEntityId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "legalEntityCode": {
            "type": "string",
            "nullable": true
          },
          "orgUnitCode": {
            "type": "string",
            "nullable": true
          },
          "code": {
            "type": "string",
            "nullable": true
          },
          "campaignKind": {
            "type": "string",
            "nullable": true
          },
          "jobTitle": {
            "type": "string",
            "nullable": true
          },
          "jobGrade": {
            "type": "string",
            "nullable": true
          },
          "employmentType": {
            "type": "string",
            "nullable": true
          },
          "headcount": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "reasonCode": {
            "type": "string",
            "nullable": true
          },
          "defaultContractTypeCode": {
            "type": "string",
            "nullable": true
          },
          "budgetMin": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "budgetMax": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "budgetCurrency": {
            "type": "string",
            "nullable": true
          },
          "targetDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "requestedBy": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "TaoHoSoRequest": {
        "type": "object",
        "properties": {
          "candidateId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "requisitionId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "sourceCode": {
            "type": "string",
            "nullable": true
          },
          "stageCode": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "TaoUngVienRequest": {
        "type": "object",
        "properties": {
          "ownerLegalEntityId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "ownerLegalEntityCode": {
            "type": "string",
            "nullable": true
          },
          "candidateCode": {
            "type": "string",
            "nullable": true
          },
          "fullName": {
            "type": "string",
            "nullable": true
          },
          "fullNameLocal": {
            "type": "string",
            "nullable": true
          },
          "dateOfBirth": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "gender": {
            "type": "string",
            "nullable": true
          },
          "nationality": {
            "type": "string",
            "nullable": true
          },
          "residenceCountry": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "nationalId": {
            "type": "string",
            "nullable": true
          },
          "nationalIdType": {
            "type": "string",
            "nullable": true
          },
          "sourceCode": {
            "type": "string",
            "nullable": true
          },
          "referredByEmployeeId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "cvFileId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "consent": {
            "$ref": "#/components/schemas/DongYRequest"
          }
        },
        "additionalProperties": false
      }
    }
  },
  "tags": [
    {
      "name": "Cơ cấu tổ chức"
    },
    {
      "name": "Công thức"
    },
    {
      "name": "Cổng tích hợp"
    },
    {
      "name": "Hệ thống"
    },
    {
      "name": "Hồ sơ nhân sự"
    },
    {
      "name": "Hội nhập"
    },
    {
      "name": "Hợp đồng lao động"
    },
    {
      "name": "Khởi tạo"
    },
    {
      "name": "Quản trị hệ thống"
    },
    {
      "name": "Tuyển dụng"
    },
    {
      "name": "Xác thực"
    }
  ]
}