openapi: 3.0.3
info:
  title: Boğaziçi PDKS API
  description: |
    Boğaziçi PDKS — Personel Devam Kontrol Sistemi REST API.

    **Auth:** RS256 JWT (15 dk access + 7 gün refresh) veya
    Ed25519 biyometrik passkey imzası.

    **Tenant:** JWT claims içinde `company_id` ile tenant izolasyonu
    PostgreSQL RLS üzerinden uygulanır.
  version: 1.0.0
  contact:
    name: Boğaziçi PDKS
servers:
  - url: http://localhost:8080
    description: Geliştirme (Homebrew PHP, port 8080)
  - url: https://api.pdks.io
    description: Üretim (gelecekte)

tags:
  - name: Auth
    description: Kimlik doğrulama, JWT, biyometrik passkey
  - name: Invite
    description: QR davet kodları + bekleyen başvurular
  - name: Employee
    description: Personel CRUD
  - name: Me
    description: Self-service — aktif çalışanın kendi verisi (clock in/out, vardiya, izin)
  - name: App
    description: Uygulama metadata (sürüm, güncelleme)

security:
  - bearerAuth: []

paths:
  # ─────────────────────────────────────────────
  # AUTH
  # ─────────────────────────────────────────────
  /api/v1/auth/login:
    post:
      tags: [Auth]
      summary: Email + şifre ile giriş
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LoginRequest' }
      responses:
        '200':
          description: Giriş başarılı
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/auth/refresh:
    post:
      tags: [Auth]
      summary: Refresh token ile yeni access token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RefreshRequest' }
      responses:
        '200':
          description: Token yenilendi
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [access_token, refresh_token]
                    properties:
                      access_token: { type: string }
                      refresh_token: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/auth/logout:
    post:
      tags: [Auth]
      summary: Tüm refresh token'ları iptal et
      responses:
        '200':
          description: Çıkış yapıldı
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MessageResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/auth/me:
    get:
      tags: [Auth]
      summary: Aktif kullanıcı bilgileri
      responses:
        '200':
          description: Kullanıcı
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/MeData' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/auth/challenge:
    post:
      tags: [Auth]
      summary: Biyometrik için tek-kullanımlık challenge üret
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ChallengeRequest' }
      responses:
        '200':
          description: Challenge döndü
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/ChallengeData' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/auth/biometric-login:
    post:
      tags: [Auth]
      summary: Ed25519 imzayla biyometrik giriş
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BiometricLoginRequest' }
      responses:
        '200':
          description: Giriş başarılı
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/v1/auth/device/register:
    post:
      tags: [Auth]
      summary: Yeni biyometrik cihaz kaydet
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeviceRegisterRequest' }
      responses:
        '200':
          description: Cihaz kaydedildi
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MessageResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/auth/devices:
    get:
      tags: [Auth]
      summary: Aktif kullanıcının cihazlarını listele
      responses:
        '200':
          description: Cihaz listesi
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Device' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /api/v1/auth/device/{device_id}:
    delete:
      tags: [Auth]
      summary: Cihazı iptal et
      parameters:
        - name: device_id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: İptal edildi
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MessageResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ─────────────────────────────────────────────
  # INVITE (PUBLIC)
  # ─────────────────────────────────────────────
  /api/v1/public/invite/{code}/info:
    get:
      tags: [Invite]
      summary: QR kodun firma bilgilerini ver
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Firma bilgisi
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/InviteInfo' }
        '404': { $ref: '#/components/responses/NotFound' }
        '410': { $ref: '#/components/responses/Gone' }

  /api/v1/public/invite/{code}/apply:
    post:
      tags: [Invite]
      summary: QR ile başvuru gönder (passkey veya şifre)
      security: []
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InviteApplyRequest' }
      responses:
        '201':
          description: Başvuru oluşturuldu
          content:
            application/json:
              schema:
                type: object
                required: [data, message]
                properties:
                  data: { $ref: '#/components/schemas/InviteApplyResult' }
                  message: { type: string }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # ─────────────────────────────────────────────
  # INVITE (ADMIN)
  # ─────────────────────────────────────────────
  /api/v1/invite-codes:
    get:
      tags: [Invite]
      summary: Firma davet kodlarını listele
      responses:
        '200':
          description: Liste
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InviteCode' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Invite]
      summary: Yeni davet kodu oluştur
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InviteCodeCreate' }
      responses:
        '201':
          description: Oluşturuldu
          content:
            application/json:
              schema:
                type: object
                required: [data, message]
                properties:
                  data: { $ref: '#/components/schemas/InviteCodeCreated' }
                  message: { type: string }

  /api/v1/invite-codes/{id}:
    delete:
      tags: [Invite]
      summary: Davet kodunu sil (hard delete)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Silindi
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MessageResponse' }

  /api/v1/users/pending:
    get:
      tags: [Invite]
      summary: Bekleyen başvuruları listele
      responses:
        '200':
          description: Liste
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/PendingUser' }
                  meta:
                    type: object
                    properties:
                      count: { type: integer }

  /api/v1/users/{id}/approve:
    post:
      tags: [Invite]
      summary: Bekleyen kullanıcıyı onayla (employee oluştur + aktive et)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Onaylandı
          content:
            application/json:
              schema:
                type: object
                required: [data, message]
                properties:
                  data:
                    type: object
                    required: [id, employee_id, email]
                    properties:
                      id: { type: integer }
                      employee_id: { type: integer }
                      email: { type: string }
                  message: { type: string }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/users/{id}/reject:
    post:
      tags: [Invite]
      summary: Bekleyen kullanıcıyı reddet
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Reddedildi
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MessageResponse' }

  # ─────────────────────────────────────────────
  # EMPLOYEE
  # ─────────────────────────────────────────────
  /api/v1/employees:
    get:
      tags: [Employee]
      summary: Personelleri listele (sayfalı)
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, default: 25 }
      responses:
        '200':
          description: Liste
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Employee' }
                  meta: { $ref: '#/components/schemas/PaginationMeta' }
    post:
      tags: [Employee]
      summary: Yeni personel oluştur
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmployeeCreate' }
      responses:
        '201':
          description: Oluşturuldu
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                  message: { type: string }

  /api/v1/employees/{id}:
    get:
      tags: [Employee]
      summary: Personel detayı
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Detay
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/EmployeeDetail' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Employee]
      summary: Personel güncelle
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmployeeUpdate' }
      responses:
        '200':
          description: Güncellendi
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MessageResponse' }
    delete:
      tags: [Employee]
      summary: Personel sil (soft delete)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Silindi
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MessageResponse' }

  # ─────────────────────────────────────────────
  # ME (self-service)
  # ─────────────────────────────────────────────
  /api/v1/me/today:
    get:
      tags: [Me]
      summary: Bugünün özeti — vardiya, attendance durumu, hafta stats, son hareketler
      responses:
        '200':
          description: Snapshot
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/MeTodaySnapshot' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/v1/me/clock-in:
    post:
      tags: [Me]
      summary: İşe giriş kaydet (opsiyonel GPS doğrulamasıyla)
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ClockRequest' }
      responses:
        '200':
          description: Giriş kaydedildi
          content:
            application/json:
              schema:
                type: object
                required: [data, message]
                properties:
                  data: { $ref: '#/components/schemas/ClockInResult' }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/me/clock-out:
    post:
      tags: [Me]
      summary: İşe çıkış kaydet (opsiyonel GPS)
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ClockRequest' }
      responses:
        '200':
          description: Çıkış kaydedildi
          content:
            application/json:
              schema:
                type: object
                required: [data, message]
                properties:
                  data: { $ref: '#/components/schemas/ClockOutResult' }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/me/access-logs:
    get:
      tags: [Me]
      summary: Kişisel geçiş geçmişi
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
      responses:
        '200':
          description: Geçiş listesi
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/AccessLogEntry' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ─────────────────────────────────────────────
  # APP META
  # ─────────────────────────────────────────────
  /api/v1/app/version:
    get:
      tags: [App]
      summary: Sürüm manifesti (in-app updater için)
      security: []
      responses:
        '200':
          description: Manifest
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/AppVersion' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  responses:
    Unauthorized:
      description: Yetkilendirme gerekli
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Forbidden:
      description: Yetkisiz erişim
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    NotFound:
      description: Bulunamadı
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Conflict:
      description: Çakışma
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Gone:
      description: Süresi doldu / kullanım limiti aşıldı
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    ValidationError:
      description: Doğrulama hatası
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ValidationErrorResponse' }

  schemas:
    # ─── Common ───
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error: { type: string }
        code: { type: integer, nullable: true }

    ValidationErrorResponse:
      type: object
      required: [error]
      properties:
        error: { type: string }
        details:
          type: object
          additionalProperties: { type: string }

    MessageResponse:
      type: object
      required: [message]
      properties:
        message: { type: string }

    PaginationMeta:
      type: object
      properties:
        page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
        last_page: { type: integer }

    # ─── Auth ───
    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email: { type: string, format: email }
        password: { type: string, minLength: 9 }

    RefreshRequest:
      type: object
      required: [refresh_token]
      properties:
        refresh_token: { type: string }

    UserData:
      type: object
      required: [id, email, role, company_id]
      properties:
        id: { type: integer }
        email: { type: string, format: email }
        role:
          type: string
          enum: [super_admin, company_admin, hr_manager, branch_manager, employee]
        company_id: { type: integer }
        company_name: { type: string }
        first_login: { type: boolean }
        is_2fa_enabled: { type: boolean }

    AuthResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [access_token, refresh_token, user]
          properties:
            access_token: { type: string }
            refresh_token: { type: string }
            user: { $ref: '#/components/schemas/UserData' }

    MeData:
      type: object
      properties:
        user_id: { type: integer }
        company_id: { type: integer }
        role: { type: string }
        employee_id: { type: integer, nullable: true }

    ChallengeRequest:
      type: object
      required: [device_id]
      properties:
        device_id: { type: string }

    ChallengeData:
      type: object
      required: [challenge, expires_in]
      properties:
        challenge: { type: string, description: "Base64 encoded random nonce" }
        expires_in: { type: integer, description: "TTL saniye cinsinden" }

    BiometricLoginRequest:
      type: object
      required: [device_id, signature]
      properties:
        device_id: { type: string }
        signature: { type: string, description: "Base64 Ed25519 signature" }

    DeviceRegisterRequest:
      type: object
      required: [device_id, public_key, platform]
      properties:
        device_id: { type: string }
        public_key: { type: string, description: "Base64 Ed25519 public key (32 bytes)" }
        platform:
          type: string
          enum: [ios, android, web]
        device_name: { type: string, nullable: true }

    Device:
      type: object
      required: [id, device_id, platform]
      properties:
        id: { type: integer }
        device_id: { type: string }
        platform:
          type: string
          enum: [ios, android, web]
        device_name: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        last_used_at: { type: string, format: date-time, nullable: true }

    # ─── Invite ───
    InviteInfo:
      type: object
      required: [mode, company_name, role]
      properties:
        mode:
          type: string
          enum: [register, add_device]
        company_name: { type: string }
        label: { type: string, nullable: true }
        role: { type: string }
        target_user:
          type: object
          nullable: true
          properties:
            email: { type: string }
            first_name: { type: string }
            last_name: { type: string }

    InviteApplyRequest:
      type: object
      properties:
        email: { type: string, format: email, nullable: true }
        password: { type: string, minLength: 9, nullable: true }
        first_name: { type: string, nullable: true }
        last_name: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        public_key: { type: string, nullable: true }
        device_id: { type: string, nullable: true }
        platform:
          type: string
          enum: [ios, android, web]
          nullable: true
        device_name: { type: string, nullable: true }

    InviteApplyResult:
      type: object
      required: [user_id, mode]
      properties:
        user_id: { type: integer }
        mode:
          type: string
          enum: [register, add_device, password]

    InviteCode:
      type: object
      required: [id, code, default_role, used_count]
      properties:
        id: { type: integer }
        code: { type: string }
        label: { type: string, nullable: true }
        default_role: { type: string }
        default_branch_id: { type: integer, nullable: true }
        default_department_id: { type: integer, nullable: true }
        max_uses: { type: integer, nullable: true }
        used_count: { type: integer }
        expires_at: { type: string, format: date-time, nullable: true }
        revoked_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        branch_name: { type: string, nullable: true }
        department_name: { type: string, nullable: true }
        created_by_email: { type: string, nullable: true }
        pending_count: { type: integer, nullable: true }

    InviteCodeCreate:
      type: object
      required: [default_role]
      properties:
        label: { type: string, nullable: true }
        default_role: { type: string }
        default_branch_id: { type: integer, nullable: true }
        default_department_id: { type: integer, nullable: true }
        max_uses: { type: integer, nullable: true }
        expires_days: { type: integer, nullable: true }

    InviteCodeCreated:
      type: object
      required: [id, code]
      properties:
        id: { type: integer }
        code: { type: string }
        expires_at: { type: string, format: date-time, nullable: true }

    PendingUser:
      type: object
      required: [id, email, first_name, last_name, created_at]
      properties:
        id: { type: integer }
        email: { type: string }
        phone: { type: string, nullable: true }
        first_name: { type: string }
        last_name: { type: string }
        created_at: { type: string, format: date-time }
        invite_code: { type: string, nullable: true }
        invite_label: { type: string, nullable: true }
        has_password: { type: boolean }
        device_id: { type: string, nullable: true }
        platform: { type: string, nullable: true }
        device_name: { type: string, nullable: true }

    # ─── Employee ───
    Employee:
      type: object
      required: [id, employee_no, first_name, last_name, status]
      properties:
        id: { type: integer }
        employee_no: { type: string }
        first_name: { type: string }
        last_name: { type: string }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        status: { type: string }
        hire_date: { type: string, format: date }
        branch_name: { type: string, nullable: true }
        department_name: { type: string, nullable: true }
        position_name: { type: string, nullable: true }

    EmployeeDetail:
      allOf:
        - $ref: '#/components/schemas/Employee'
        - type: object
          properties:
            company_id: { type: integer }
            branch_id: { type: integer }
            department_id: { type: integer, nullable: true }
            position_id: { type: integer, nullable: true }
            gender: { type: string, nullable: true }
            birth_date: { type: string, format: date, nullable: true }
            termination_date: { type: string, format: date, nullable: true }
            photo_url: { type: string, nullable: true }
            user_id: { type: integer, nullable: true }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }

    EmployeeCreate:
      type: object
      required: [branch_id, employee_no, first_name, last_name, hire_date]
      properties:
        branch_id: { type: integer }
        department_id: { type: integer, nullable: true }
        position_id: { type: integer, nullable: true }
        employee_no: { type: string, maxLength: 50 }
        first_name: { type: string, maxLength: 100 }
        last_name: { type: string, maxLength: 100 }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        gender: { type: string, nullable: true }
        birth_date: { type: string, format: date, nullable: true }
        hire_date: { type: string, format: date }

    EmployeeUpdate:
      type: object
      properties:
        branch_id: { type: integer, nullable: true }
        department_id: { type: integer, nullable: true }
        position_id: { type: integer, nullable: true }
        first_name: { type: string, nullable: true }
        last_name: { type: string, nullable: true }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        gender: { type: string, nullable: true }
        birth_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true }

    # ─── Me ───
    MeEmployee:
      type: object
      required: [id, employee_no, first_name, last_name]
      properties:
        id: { type: integer }
        employee_no: { type: string }
        first_name: { type: string }
        last_name: { type: string }
        branch_name: { type: string, nullable: true }
        department_name: { type: string, nullable: true }
        position_name: { type: string, nullable: true }

    MeShift:
      type: object
      required: [id, name, color, start_time, end_time, break_minutes]
      properties:
        id: { type: integer }
        name: { type: string }
        color: { type: string }
        start_time: { type: string, description: "HH:MM" }
        end_time: { type: string, description: "HH:MM" }
        break_minutes: { type: integer }

    MeAttendance:
      type: object
      properties:
        first_in: { type: string, format: date-time, nullable: true }
        last_out: { type: string, format: date-time, nullable: true }
        gross_minutes: { type: integer, nullable: true }
        net_minutes: { type: integer, nullable: true }
        late_minutes: { type: integer }

    MeTodayBlock:
      type: object
      required: [date, state]
      properties:
        date: { type: string, format: date }
        state:
          type: string
          enum: [awaiting_clock_in, clocked_in, clocked_out, no_shift]
        shift:
          allOf:
            - { $ref: '#/components/schemas/MeShift' }
          nullable: true
        attendance:
          allOf:
            - { $ref: '#/components/schemas/MeAttendance' }
          nullable: true

    MeWeekStats:
      type: object
      required: [total_minutes, late_count, present_days]
      properties:
        total_minutes: { type: integer }
        late_count: { type: integer }
        present_days: { type: integer }

    AccessLogEntry:
      type: object
      required: [id, direction, scan_method, scanned_at]
      properties:
        id: { type: integer }
        direction:
          type: string
          enum: [in, out, both]
        scan_method: { type: string }
        scanned_at: { type: string, format: date-time }
        access_point_name: { type: string, nullable: true }
        branch_name: { type: string, nullable: true }
        is_valid: { type: boolean, nullable: true }

    MeTodaySnapshot:
      type: object
      required: [employee, today, week, recent_logs]
      properties:
        employee: { $ref: '#/components/schemas/MeEmployee' }
        today: { $ref: '#/components/schemas/MeTodayBlock' }
        week: { $ref: '#/components/schemas/MeWeekStats' }
        recent_logs:
          type: array
          items: { $ref: '#/components/schemas/AccessLogEntry' }

    ClockRequest:
      type: object
      properties:
        latitude: { type: number, format: double, nullable: true }
        longitude: { type: number, format: double, nullable: true }
        accuracy: { type: number, format: double, nullable: true }

    ClockInResult:
      type: object
      required: [state, late_minutes]
      properties:
        state: { type: string, enum: [clocked_in] }
        first_in: { type: string, format: date-time, nullable: true }
        late_minutes: { type: integer }

    ClockOutResult:
      type: object
      required: [state]
      properties:
        state: { type: string, enum: [clocked_out] }
        last_out: { type: string, format: date-time, nullable: true }
        net_minutes: { type: integer, nullable: true }

    # ─── App ───
    AppVersion:
      type: object
      required: [latest_version, build_number, apk_url]
      properties:
        latest_version: { type: string }
        build_number: { type: integer }
        min_supported_version: { type: string, nullable: true }
        mandatory: { type: boolean }
        apk_url: { type: string }
        size_bytes: { type: integer }
        release_notes:
          type: array
          items: { type: string }
