{
  "openapi": "3.1.0",
  "info": {
    "title": "iClinic Integration API",
    "version": "1.0.0",
    "summary": "Đọc danh sách lượt khám của phòng khám bằng một API key cố định.",
    "description": "API **chỉ đọc** cho phần mềm bên ngoài (ví dụ Tele-sale) lấy danh sách lượt khám theo khoảng ngày: đối chiếu lịch hẹn (Đã đến / Đã huỷ) và lập danh sách gọi chăm sóc sau khám.\n\nXác thực bằng một API key cố định dạng `ick_…`, do quản trị viên phòng khám tạo trong **Quản trị → Kết nối ngoài (API)**. Mỗi key chỉ đọc được dữ liệu của phòng khám đã tạo ra nó.",
    "contact": {
      "name": "iClinic",
      "url": "https://iclinic.phongkhamhieploi.vn/#demo"
    }
  },
  "servers": [
    {
      "url": "https://iclinic.phongkhamhieploi.vn/api/integration/v1",
      "description": "Production"
    }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [{ "name": "Encounters", "description": "Lượt khám của phòng khám." }],
  "paths": {
    "/encounters": {
      "get": {
        "operationId": "listEncounters",
        "tags": ["Encounters"],
        "summary": "Liệt kê lượt khám theo khoảng ngày",
        "description": "Trả về các lượt khám có `visitDate` nằm trong `[from, to]`, mới nhất trước. Duyệt hết bằng cách tăng `page` cho tới khi đủ `total`.\n\nMặc định **không** gồm lượt khám `CANCELLED`. Để nhận cả lượt đã huỷ, gọi với `status=all` hoặc `status=CANCELLED`.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "Ngày khám bắt đầu (giờ phòng khám), `YYYY-MM-DD`.",
            "schema": { "type": "string", "format": "date" },
            "example": "2026-09-14"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Ngày khám kết thúc, gồm cả ngày này. Mặc định là hôm nay. Khoảng `from`–`to` tối đa 31 ngày.",
            "schema": { "type": "string", "format": "date" },
            "example": "2026-09-19"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Lọc theo trạng thái. Bỏ trống: mọi trạng thái trừ `CANCELLED`. `all`: mọi trạng thái, gồm cả `CANCELLED`. Không phân biệt hoa thường.",
            "schema": {
              "type": "string",
              "enum": ["NEW", "ONGOING", "FINISHED", "REOPENED", "LOCKED", "CANCELLED", "all"]
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Số trang, bắt đầu từ 0.",
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "description": "Số lượt khám mỗi trang.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 200 }
          }
        ],
        "responses": {
          "200": {
            "description": "Danh sách lượt khám.",
            "headers": {
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/EncounterList" },
                "examples": {
                  "ongoing": {
                    "summary": "Một lượt khám đang diễn ra",
                    "value": {
                      "requestId": "req_7c1f3a",
                      "from": "2026-09-14",
                      "to": "2026-09-19",
                      "page": 0,
                      "pageSize": 200,
                      "total": 1,
                      "encounters": [
                        {
                          "id": "BA-000123",
                          "encounterUuid": "3f0c9a52-6d1b-4e7a-9b1e-2a4c8d5e7f10",
                          "patientName": "Nguyễn Văn A",
                          "phone": "0900000001",
                          "visitDate": "2026-09-19",
                          "checkedInAt": "2026-09-19T02:14:05.000Z",
                          "completedAt": null,
                          "status": "ONGOING",
                          "patientType": "OLD",
                          "diagnosis": "Tăng huyết áp",
                          "icd10Code": "I10"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ick_…",
        "description": "API key cố định, gửi trong header `Authorization: Bearer ick_…`. Key không tự hết hạn, chỉ mất hiệu lực khi quản trị viên thu hồi."
      }
    },
    "headers": {
      "XRequestId": {
        "description": "Mã của yêu cầu, trùng với `requestId` trong thân phản hồi. Gửi kèm khi liên hệ hỗ trợ.",
        "schema": { "type": "string" }
      },
      "RetryAfter": {
        "description": "Số giây cần chờ trước khi gọi lại.",
        "schema": { "type": "integer", "minimum": 1 }
      }
    },
    "schemas": {
      "EncounterStatus": {
        "type": "string",
        "description": "`NEW` đã đến / tiếp nhận · `ONGOING` đang khám · `FINISHED` đã khám xong · `REOPENED` mở lại · `LOCKED` đã khoá hồ sơ · `CANCELLED` đã huỷ.",
        "enum": ["NEW", "ONGOING", "FINISHED", "REOPENED", "LOCKED", "CANCELLED"]
      },
      "PatientType": {
        "type": "string",
        "description": "`NEW` khách mới · `OLD` khách cũ · `RE_EXAMINATION` tái khám theo hẹn.",
        "enum": ["NEW", "OLD", "RE_EXAMINATION"]
      },
      "Encounter": {
        "type": "object",
        "required": [
          "id",
          "encounterUuid",
          "patientName",
          "phone",
          "visitDate",
          "checkedInAt",
          "completedAt",
          "status",
          "patientType",
          "diagnosis",
          "icd10Code"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Mã hiển thị (mã hồ sơ). Chỉ để tham chiếu; có thể được cấp lại — không dùng làm khoá chống trùng.",
            "examples": ["BA-000123"]
          },
          "encounterUuid": {
            "type": "string",
            "description": "Mã ổn định của lượt khám, không bao giờ đổi. Dùng làm khoá chống trùng.",
            "examples": ["3f0c9a52-6d1b-4e7a-9b1e-2a4c8d5e7f10"]
          },
          "patientName": { "type": "string", "description": "Tên bệnh nhân." },
          "phone": {
            "type": ["string", "null"],
            "description": "Số điện thoại; `null` nếu phòng khám chưa có số."
          },
          "visitDate": {
            "type": "string",
            "format": "date",
            "description": "Ngày khám (giờ phòng khám). Hệ thống lưu ngày, không lưu giờ khám."
          },
          "checkedInAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Thời điểm tiếp nhận (UTC) khi biết được, ngược lại `null`."
          },
          "completedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Thời điểm kết thúc khám (UTC) hoặc `null`."
          },
          "status": { "$ref": "#/components/schemas/EncounterStatus" },
          "patientType": { "$ref": "#/components/schemas/PatientType" },
          "diagnosis": {
            "type": "string",
            "description": "Chẩn đoán; chuỗi rỗng nếu chưa có."
          },
          "icd10Code": {
            "type": "string",
            "description": "Mã ICD-10 của chẩn đoán; chuỗi rỗng nếu chưa có."
          }
        }
      },
      "EncounterList": {
        "type": "object",
        "required": ["requestId", "from", "to", "page", "pageSize", "total", "encounters"],
        "properties": {
          "requestId": { "type": "string" },
          "from": { "type": "string", "format": "date" },
          "to": { "type": "string", "format": "date" },
          "page": { "type": "integer", "minimum": 0 },
          "pageSize": { "type": "integer", "minimum": 1, "maximum": 500 },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Tổng số lượt khám khớp bộ lọc, trên mọi trang."
          },
          "encounters": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Encounter" }
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "enum": [
          "INVALID_DATE",
          "INVALID_RANGE",
          "INVALID_STATUS",
          "INVALID_PAGE",
          "UNAUTHORIZED",
          "METHOD_NOT_ALLOWED",
          "RATE_LIMITED"
        ]
      },
      "Error": {
        "type": "object",
        "required": ["requestId", "error"],
        "properties": {
          "requestId": { "type": "string" },
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "$ref": "#/components/schemas/ErrorCode" },
              "message": {
                "type": "string",
                "description": "Mô tả lỗi bằng tiếng Việt, dành cho người đọc. Xử lý theo `code`, không theo `message`."
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Tham số không hợp lệ: `INVALID_DATE`, `INVALID_RANGE`, `INVALID_STATUS` hoặc `INVALID_PAGE`.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": {
                "code": "INVALID_RANGE",
                "message": "`to` phải sau `from` và khoảng tối đa 31 ngày."
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Thiếu, sai hoặc key đã bị thu hồi; hoặc phòng khám đang bị tạm ngưng.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": { "code": "UNAUTHORIZED", "message": "API key không hợp lệ." }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Chỉ hỗ trợ `GET`.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": { "code": "METHOD_NOT_ALLOWED", "message": "Chỉ hỗ trợ GET." }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Vượt giới hạn 60 yêu cầu/phút cho mỗi key, hoặc IP đang bị khoá tạm vì gửi sai key nhiều lần. Chờ theo `Retry-After` rồi thử lại.",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "requestId": "req_7c1f3a",
              "error": {
                "code": "RATE_LIMITED",
                "message": "Vượt giới hạn số yêu cầu. Vui lòng thử lại sau."
              }
            }
          }
        }
      }
    }
  }
}
