{
  "openapi": "3.0.3",
  "info": {
    "title": "Kliniktech External API",
    "version": "1.0.0",
    "description": "API integrasi sistem eksternal untuk Kliniktech Cloud. Base URL produksi: https://apps.ion.net.id/api"
  },
  "servers": [
    {
      "url": "https://apps.ion.net.id/api",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "CustomerCreate": {
        "type": "object",
        "required": [
          "name",
          "phone",
          "address"
        ],
        "properties": {
          "name": {
            "type": "string",
            "example": "Budi Santoso"
          },
          "phone": {
            "type": "string",
            "example": "081234567890"
          },
          "email": {
            "type": "string",
            "format": "email",
            "nullable": true
          },
          "address": {
            "type": "string",
            "example": "Jl. Merdeka No. 10"
          },
          "latitude": {
            "type": "number",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "nullable": true
          },
          "package_id": {
            "type": "integer",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "terminated"
            ],
            "nullable": true
          },
          "service_type": {
            "type": "string",
            "enum": [
              "pppoe",
              "hotspot",
              "dhcp",
              "static"
            ],
            "nullable": true
          },
          "billing_cycle": {
            "type": "string",
            "enum": [
              "prepaid",
              "prepaid_rolling",
              "postpaid"
            ],
            "nullable": true
          }
        }
      },
      "SalesItem": {
        "type": "object",
        "required": [
          "name",
          "quantity"
        ],
        "properties": {
          "inventory_item_id": {
            "type": "integer",
            "nullable": true,
            "description": "Isi jika item berasal dari Gudang/Inventory."
          },
          "item_type": {
            "type": "string",
            "enum": [
              "service",
              "goods",
              "material",
              "device"
            ],
            "example": "service"
          },
          "name": {
            "type": "string",
            "example": "Jasa tarik kabel FO"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "quantity": {
            "type": "number",
            "minimum": 0.01,
            "example": 1
          },
          "unit": {
            "type": "string",
            "example": "paket"
          },
          "unit_price": {
            "type": "number",
            "minimum": 0,
            "example": 2500000
          },
          "unit_cost": {
            "type": "number",
            "minimum": 0,
            "example": 1500000
          },
          "discount_amount": {
            "type": "number",
            "minimum": 0
          },
          "tax_amount": {
            "type": "number",
            "minimum": 0
          }
        }
      },
      "TicketCreate": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "customer_id": {
            "type": "integer",
            "nullable": true
          },
          "onu_id": {
            "type": "integer",
            "nullable": true
          },
          "type": {
            "type": "string",
            "example": "Internet lambat"
          },
          "priority": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ],
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "inprogress"
            ],
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "location_text": {
            "type": "string",
            "nullable": true
          },
          "contact_name": {
            "type": "string",
            "nullable": true
          },
          "contact_phone": {
            "type": "string",
            "nullable": true
          },
          "external_id": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "TokenCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "example": "CRM Production"
          },
          "access": {
            "type": "string",
            "enum": [
              "read",
              "read_write"
            ],
            "default": "read"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "TokenInfo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "access": {
            "type": "string",
            "enum": [
              "read",
              "read_write"
            ]
          },
          "created_by": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer",
                "nullable": true
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "email": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "abilities": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "MasterFileShare": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "apk-netadmin-v1-2-0.apk"
          },
          "folder": {
            "type": "string",
            "example": "apk"
          },
          "disk": {
            "type": "string",
            "enum": [
              "s3",
              "local"
            ]
          },
          "storage_label": {
            "type": "string",
            "example": "S3 Object Storage"
          },
          "mime_type": {
            "type": "string",
            "nullable": true
          },
          "size_bytes": {
            "type": "integer"
          },
          "size_mb": {
            "type": "number"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "is_public": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "TenantFileManagerFile": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "tenant_id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "example": "dokumen-onboarding.pdf"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "private",
              "shared"
            ]
          },
          "visibility_label": {
            "type": "string",
            "example": "Folder pribadi"
          },
          "folder": {
            "type": "string",
            "example": "dokumen"
          },
          "disk": {
            "type": "string",
            "enum": [
              "s3",
              "local"
            ]
          },
          "storage_label": {
            "type": "string",
            "example": "S3 Object Storage"
          },
          "mime_type": {
            "type": "string",
            "nullable": true
          },
          "size_bytes": {
            "type": "integer"
          },
          "size_mb": {
            "type": "number"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "owner": {
            "type": "object",
            "nullable": true
          },
          "uploaded_by": {
            "type": "object",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "WaBotConfigUpdate": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "wablas",
              "fonnte",
              "watzap",
              "balesotomatis",
              "pancake",
              "waba",
              "twilio"
            ]
          },
          "wa_token": {
            "type": "string",
            "description": "API token provider. Untuk WatZap: API Key. Untuk Pancake: Page Access Token."
          },
          "wa_secret_key": {
            "type": "string",
            "description": "Secret/number key provider. Untuk WatZap dikosongkan. Untuk Pancake: Page ID."
          },
          "wa_number": {
            "type": "string",
            "description": "Nomor default test. Untuk Pancake: Conversation ID default test."
          },
          "settings": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "auto_reply": {
                "type": "boolean"
              },
              "base_url": {
                "type": "string",
                "format": "uri",
                "example": "https://pages.fm"
              },
              "ignore_groups": {
                "type": "boolean"
              },
              "webhook_auto_register": {
                "type": "boolean"
              },
              "reply_unknown_customers": {
                "type": "boolean"
              },
              "max_messages_per_minute": {
                "type": "integer",
                "minimum": 0,
                "maximum": 1000
              },
              "message_delay_seconds": {
                "type": "integer",
                "minimum": 0,
                "maximum": 3600
              },
              "routing_strategy": {
                "type": "string",
                "enum": [
                  "auto"
                ],
                "description": "Routing otomatis: satu akun aktif dipakai langsung, beberapa akun aktif dikirim bergantian round-robin."
              },
              "accounts": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "provider": {
                      "type": "string",
                      "enum": [
                        "wablas",
                        "fonnte",
                        "watzap",
                        "balesotomatis",
                        "pancake",
                        "waba",
                        "twilio"
                      ]
                    },
                    "label": {
                      "type": "string"
                    },
                    "token": {
                      "type": "string"
                    },
                    "secret_key": {
                      "type": "string"
                    },
                    "number": {
                      "type": "string"
                    },
                    "base_url": {
                      "type": "string"
                    },
                    "enabled": {
                      "type": "boolean"
                    },
                    "weight": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 100
                    },
                    "max_messages_per_minute": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 1000
                    },
                    "message_delay_seconds": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 3600
                    }
                  }
                }
              }
            }
          }
        }
      },
      "WaBotTestRequest": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string",
            "description": "Nomor tujuan. Untuk Pancake: Conversation ID."
          },
          "message": {
            "type": "string",
            "maxLength": 500
          },
          "account_id": {
            "type": "string",
            "maxLength": 80,
            "description": "ID akun WhatsApp provider yang ingin dites. Jika kosong, sistem memakai provider/routing aktif tenant."
          }
        }
      },
      "TenantProvisioningRequest": {
        "type": "object",
        "properties": {
          "subdomain": {
            "type": "string",
            "example": "abcnet"
          },
          "domain": {
            "type": "string",
            "example": "kliniktech.com"
          },
          "dns_target": {
            "type": "string",
            "example": "151.244.238.77"
          },
          "record_type": {
            "type": "string",
            "enum": [
              "A",
              "CNAME"
            ],
            "default": "A"
          },
          "proxied": {
            "type": "boolean",
            "default": true
          },
          "mailbox_local_part": {
            "type": "string",
            "example": "admin"
          },
          "mailbox_quota_mb": {
            "type": "integer",
            "example": 1024
          }
        }
      },
      "TenantProvisioningPilotDbRequest": {
        "type": "object",
        "properties": {
          "subdomain": {
            "type": "string",
            "example": "pilot-demo"
          },
          "domain": {
            "type": "string",
            "example": "kliniktech.com"
          },
          "db_user_host": {
            "type": "string",
            "example": "%"
          },
          "confirm_phrase": {
            "type": "string",
            "description": "Required only for run endpoint. Must be RUN DB PILOT."
          }
        }
      },
      "TenantProvisioningCredentialRequest": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "backup_ready": {
            "type": "boolean"
          },
          "tenant_db": {
            "type": "object",
            "properties": {
              "host": {
                "type": "string"
              },
              "port": {
                "type": "integer",
                "example": 3306
              },
              "admin_user": {
                "type": "string"
              },
              "admin_password": {
                "type": "string",
                "format": "password"
              }
            }
          },
          "cloudflare": {
            "type": "object",
            "properties": {
              "api_token": {
                "type": "string",
                "format": "password"
              },
              "zone_id": {
                "type": "string"
              }
            }
          },
          "virtualmin": {
            "type": "object",
            "properties": {
              "host": {
                "type": "string"
              },
              "port": {
                "type": "integer",
                "example": 10000
              },
              "user": {
                "type": "string"
              },
              "password": {
                "type": "string",
                "format": "password"
              },
              "ssh_key_path": {
                "type": "string"
              },
              "ssl": {
                "type": "boolean"
              }
            }
          },
          "storage": {
            "type": "object",
            "properties": {
              "endpoint": {
                "type": "string"
              },
              "region": {
                "type": "string"
              },
              "bucket": {
                "type": "string"
              },
              "access_key": {
                "type": "string"
              },
              "secret_key": {
                "type": "string",
                "format": "password"
              }
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "example": "demo_guard_blocked"
          }
        }
      },
      "SupplierImportItem": {
        "type": "object",
        "properties": {
          "selected": {
            "type": "boolean"
          },
          "sku": {
            "type": "string",
            "maxLength": 80
          },
          "name": {
            "type": "string",
            "maxLength": 150
          },
          "category": {
            "type": "string",
            "maxLength": 100
          },
          "unit": {
            "type": "string",
            "maxLength": 20
          },
          "cost_price": {
            "type": "number",
            "minimum": 0
          },
          "sell_price": {
            "type": "number",
            "minimum": 0
          },
          "notes": {
            "type": "string",
            "maxLength": 500
          }
        },
        "required": [
          "name"
        ]
      }
    },
    "parameters": {
      "Page": {
        "name": "page",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "PerPage": {
        "name": "per_page",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100
        }
      },
      "Search": {
        "name": "search",
        "in": "query",
        "schema": {
          "type": "string"
        }
      },
      "UpdatedSince": {
        "name": "updated_since",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Token tidak valid, sesi berakhir, atau header Authorization tidak dikirim. Response berupa JSON dengan code UNAUTHENTICATED, bukan redirect login."
      },
      "Forbidden": {
        "description": "Token tidak punya akses ke tenant atau operasi, termasuk aksi yang diblokir Mode Demo Aman",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "demoGuard": {
                "summary": "Mode Demo Aman memblokir aksi",
                "value": {
                  "message": "Aksi ini diblokir di tenant demo agar data sandbox tetap aman.",
                  "code": "demo_guard_blocked"
                }
              }
            }
          }
        }
      },
      "ValidationError": {
        "description": "Payload tidak valid"
      },
      "NotFound": {
        "description": "Data tidak ditemukan"
      }
    }
  },
  "paths": {
    "/android-apk": {
      "get": {
        "tags": [
          "Internal APK Builder"
        ],
        "summary": "Ambil konfigurasi APK tenant, riwayat build, dan health environment.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Konfigurasi APK berhasil diambil."
          },
          "422": {
            "description": "Tenant/organisasi belum dipilih."
          }
        }
      }
    },
    "/android-apk/health": {
      "get": {
        "tags": [
          "Internal APK Builder"
        ],
        "summary": "Cek kesiapan Node.js, npm, Java/keytool, Android SDK, dan storage untuk build APK.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Status environment build APK."
          }
        }
      }
    },
    "/android-apk/configs/{appType}": {
      "put": {
        "tags": [
          "Internal APK Builder"
        ],
        "summary": "Simpan konfigurasi APK untuk portal, netadmin, partner, atau absensi.",
        "description": "Untuk appType absensi, base_url cukup domain utama aplikasi; builder otomatis mengarahkan APK ke /absensi/.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "appType",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "portal",
                "netadmin",
                "partner",
                "absensi"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "app_name",
                  "package_id",
                  "version_name",
                  "version_code",
                  "base_url",
                  "environment",
                  "theme_color",
                  "orientation",
                  "signing_mode"
                ],
                "properties": {
                  "app_name": {
                    "type": "string",
                    "example": "Kliniktech Absensi"
                  },
                  "package_id": {
                    "type": "string",
                    "example": "id.kliniktech.absensi.kliniktech.t1"
                  },
                  "version_name": {
                    "type": "string",
                    "example": "1.0.0"
                  },
                  "version_code": {
                    "type": "integer",
                    "example": 1
                  },
                  "base_url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://apps.ion.net.id"
                  },
                  "api_url": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true,
                    "example": "https://apps.ion.net.id/api"
                  },
                  "environment": {
                    "type": "string",
                    "enum": [
                      "sandbox",
                      "production"
                    ]
                  },
                  "theme_color": {
                    "type": "string",
                    "example": "#0a0f1e"
                  },
                  "orientation": {
                    "type": "string",
                    "enum": [
                      "portrait",
                      "landscape",
                      "default"
                    ]
                  },
                  "signing_mode": {
                    "type": "string",
                    "enum": [
                      "debug",
                      "release"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Konfigurasi APK disimpan."
          },
          "422": {
            "description": "Validasi gagal, misalnya app type, Package ID, Base URL, version, atau warna HEX tidak valid."
          }
        }
      }
    },
    "/android-apk/configs/{appType}/builds": {
      "post": {
        "tags": [
          "Internal APK Builder"
        ],
        "summary": "Mulai build APK debug atau release untuk app type terpilih.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "appType",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "portal",
                "netadmin",
                "partner",
                "absensi"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "build_type"
                ],
                "properties": {
                  "build_type": {
                    "type": "string",
                    "enum": [
                      "debug",
                      "release"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Build APK masuk antrian."
          },
          "422": {
            "description": "Build ditolak, misalnya build lain masih berjalan, app nonaktif, atau keystore release belum siap."
          }
        }
      }
    },
    "/sales/dashboard": {
      "get": {
        "summary": "Dashboard Penjualan",
        "tags": [
          "Admin Sales"
        ],
        "description": "KPI lead, penawaran, order, omzet, margin, dan data sales terbaru. Membutuhkan permission sales.view dan modul sales aktif.",
        "responses": {
          "200": {
            "description": "Dashboard sales berhasil dimuat"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "503": {
            "description": "Migration sales belum dijalankan"
          }
        }
      }
    },
    "/sales/options": {
      "get": {
        "summary": "Opsi status dan tipe Penjualan",
        "tags": [
          "Admin Sales"
        ],
        "responses": {
          "200": {
            "description": "Opsi sales berhasil dimuat"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/sales/leads": {
      "get": {
        "summary": "List lead Penjualan",
        "tags": [
          "Admin Sales"
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customer_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List lead sales"
          }
        }
      },
      "post": {
        "summary": "Buat lead Penjualan",
        "tags": [
          "Admin Sales"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contact_name"
                ],
                "properties": {
                  "customer_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "contact_name": {
                    "type": "string",
                    "example": "Bapak Andi"
                  },
                  "company_name": {
                    "type": "string",
                    "nullable": true
                  },
                  "phone": {
                    "type": "string",
                    "nullable": true
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "nullable": true
                  },
                  "address": {
                    "type": "string",
                    "nullable": true
                  },
                  "source": {
                    "type": "string",
                    "nullable": true
                  },
                  "need_summary": {
                    "type": "string",
                    "nullable": true
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "new",
                      "contacted",
                      "survey",
                      "quoted",
                      "won",
                      "lost"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Lead sales dibuat"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/sales/quotes": {
      "get": {
        "summary": "List penawaran Sales",
        "tags": [
          "Admin Sales"
        ],
        "responses": {
          "200": {
            "description": "List penawaran sales"
          }
        }
      },
      "post": {
        "summary": "Buat penawaran Sales",
        "tags": [
          "Admin Sales"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "items"
                ],
                "properties": {
                  "lead_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "customer_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "title": {
                    "type": "string",
                    "example": "Penawaran tarik FO dan ONU"
                  },
                  "customer_type": {
                    "type": "string",
                    "enum": [
                      "customer",
                      "non_customer"
                    ]
                  },
                  "valid_until": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/SalesItem"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Penawaran sales dibuat"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/sales/quotes/{quote}/order": {
      "post": {
        "summary": "Convert penawaran menjadi Sales Order",
        "tags": [
          "Admin Sales"
        ],
        "parameters": [
          {
            "name": "quote",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Sales order berhasil dibuat dari penawaran"
          }
        }
      }
    },
    "/sales/orders": {
      "get": {
        "summary": "List Sales Order",
        "tags": [
          "Admin Sales"
        ],
        "responses": {
          "200": {
            "description": "List sales order"
          }
        }
      },
      "post": {
        "summary": "Buat Sales Order",
        "tags": [
          "Admin Sales"
        ],
        "description": "Mendukung pelanggan existing dan non-pelanggan one-time order.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "items"
                ],
                "properties": {
                  "customer_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "title": {
                    "type": "string",
                    "example": "Penjualan ONU + Jasa Tarik Kabel FO"
                  },
                  "order_type": {
                    "type": "string",
                    "enum": [
                      "internet_installation",
                      "fo_installation",
                      "device_sales",
                      "general"
                    ]
                  },
                  "customer_type": {
                    "type": "string",
                    "enum": [
                      "customer",
                      "non_customer"
                    ]
                  },
                  "company_name": {
                    "type": "string",
                    "nullable": true
                  },
                  "contact_name": {
                    "type": "string",
                    "nullable": true
                  },
                  "phone": {
                    "type": "string",
                    "nullable": true
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "work_description": {
                    "type": "string",
                    "nullable": true
                  },
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/SalesItem"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sales order dibuat"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/sales/orders/{order}/invoice": {
      "post": {
        "summary": "Buat invoice dari Sales Order",
        "tags": [
          "Admin Sales"
        ],
        "parameters": [
          {
            "name": "order",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice sales berhasil dibuat"
          }
        }
      }
    },
    "/sales/orders/{order}/stock-out": {
      "post": {
        "summary": "Request stok keluar dari Sales Order",
        "tags": [
          "Admin Sales"
        ],
        "parameters": [
          {
            "name": "order",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source_location_id": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Request stok keluar dibuat dan menunggu approval Gudang"
          }
        }
      }
    },
    "/sales/orders/{order}/schedule": {
      "post": {
        "summary": "Buat jadwal teknisi dari Sales Order",
        "tags": [
          "Admin Sales"
        ],
        "parameters": [
          {
            "name": "order",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scheduled_at"
                ],
                "properties": {
                  "scheduled_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "job_type": {
                    "type": "string"
                  },
                  "technician_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jadwal teknisi sales dibuat"
          }
        }
      }
    },
    "/paypoint/bootstrap": {
      "get": {
        "summary": "Bootstrap portal Paypoint POP",
        "tags": [
          "Paypoint POP"
        ],
        "responses": {
          "200": {
            "description": "User kasir, tenant, home base, akun kas, dan summary hari ini"
          },
          "403": {
            "description": "Permission paypoint.view tidak tersedia"
          }
        }
      }
    },
    "/paypoint/search": {
      "get": {
        "summary": "Cari pelanggan atau invoice Paypoint",
        "tags": [
          "Paypoint POP"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "description": "Kode pelanggan, nama, nomor HP, email, atau nomor invoice"
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar pelanggan dengan invoice unpaid/overdue"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/paypoint/invoices/{invoice}/cash": {
      "post": {
        "summary": "Terima pembayaran cash Paypoint POP",
        "tags": [
          "Paypoint POP"
        ],
        "parameters": [
          {
            "name": "invoice",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "finance_account_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "cash_received": {
                    "type": "number",
                    "nullable": true
                  },
                  "payment_ref": {
                    "type": "string",
                    "nullable": true
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice lunas, PaymentRecord tercatat, masa aktif diperpanjang, dan payload struk dikembalikan"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/paypoint/transactions": {
      "get": {
        "summary": "Transaksi Paypoint hari ini",
        "tags": [
          "Paypoint POP"
        ],
        "responses": {
          "200": {
            "description": "Daftar transaksi Paypoint hari ini"
          }
        }
      }
    },
    "/paypoint/summary": {
      "get": {
        "summary": "Summary Paypoint hari ini",
        "tags": [
          "Paypoint POP"
        ],
        "responses": {
          "200": {
            "description": "Jumlah dan nominal transaksi Paypoint hari ini"
          }
        }
      }
    },
    "/auth/email-settings": {
      "get": {
        "summary": "Lihat Email Setting akun aktif",
        "tags": [
          "Admin Email Client"
        ],
        "responses": {
          "200": {
            "description": "Email Setting akun"
          },
          "503": {
            "description": "Migration external_email_accounts belum dijalankan"
          }
        }
      },
      "put": {
        "summary": "Simpan Email Setting akun",
        "tags": [
          "Admin Email Client"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "incoming_protocol",
                  "incoming_host",
                  "incoming_port",
                  "incoming_encryption",
                  "incoming_username",
                  "outgoing_host",
                  "outgoing_port",
                  "outgoing_encryption",
                  "outgoing_username"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "incoming_protocol": {
                    "type": "string",
                    "enum": [
                      "imap",
                      "pop3"
                    ]
                  },
                  "leave_on_server": {
                    "type": "boolean",
                    "description": "Dipaksa true oleh backend agar email asli tetap di server."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Email Setting disimpan"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/auth/email-settings/sync": {
      "post": {
        "summary": "Sync email masuk ke Inbox lokal",
        "tags": [
          "Admin Email Client"
        ],
        "responses": {
          "200": {
            "description": "Sync selesai"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/auth/email-settings/test-incoming": {
      "post": {
        "summary": "Test koneksi IMAP/POP3",
        "tags": [
          "Admin Email Client"
        ],
        "responses": {
          "200": {
            "description": "Test incoming berhasil"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/communications/{id}/reply-email": {
      "post": {
        "summary": "Reply email masuk memakai SMTP akun",
        "tags": [
          "Admin Email Client"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "subject",
                  "message"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "format": "email"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Balasan email terkirim"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/integrations/api-tokens": {
      "get": {
        "summary": "List token integrasi tenant aktif",
        "tags": [
          "Token Management"
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Buat token integrasi",
        "tags": [
          "Token Management"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Token dibuat"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/integrations/api-tokens/{id}": {
      "delete": {
        "summary": "Cabut token integrasi",
        "tags": [
          "Token Management"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Token dicabut"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/master/file-shares": {
      "get": {
        "summary": "List File Share tenant master",
        "tags": [
          "Internal File Share"
        ],
        "parameters": [
          {
            "name": "folder",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Search"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "summary": "Upload file ke File Share tenant master",
        "tags": [
          "Internal File Share"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "folder": {
                    "type": "string",
                    "example": "docs"
                  },
                  "description": {
                    "type": "string"
                  },
                  "is_public": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File diupload"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/master/file-shares/{id}/download": {
      "get": {
        "summary": "Download file share tenant master",
        "tags": [
          "Internal File Share"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File binary"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/master/file-shares/{id}": {
      "delete": {
        "summary": "Hapus file share tenant master",
        "tags": [
          "Internal File Share"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File dihapus"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/wabot/config": {
      "get": {
        "summary": "Ambil konfigurasi WhatsApp Bot tenant aktif",
        "tags": [
          "Internal WhatsApp Bot"
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "put": {
        "summary": "Simpan konfigurasi WhatsApp Bot tenant aktif",
        "tags": [
          "Internal WhatsApp Bot"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WaBotConfigUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Konfigurasi tersimpan"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/wabot/test": {
      "post": {
        "summary": "Test koneksi/kirim WhatsApp Bot, termasuk akun provider tertentu",
        "tags": [
          "Internal WhatsApp Bot"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WaBotTestRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Test selesai"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/wabot/sync-status": {
      "post": {
        "summary": "Sinkron status pesan Wablas dari report realtime",
        "tags": [
          "Internal WhatsApp Bot"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 50
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status dicek. Jika provider Wablas membalas error sementara, response berisi ok=false dan warning=true."
          }
        }
      }
    },
    "/wabot/sync-device-status": {
      "post": {
        "summary": "Sinkron status device Wablas dari API provider",
        "tags": [
          "Internal WhatsApp Bot"
        ],
        "responses": {
          "200": {
            "description": "Status device dicek dan disimpan"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/wabot/inbound/{tenant_slug}": {
      "post": {
        "summary": "Webhook inbound WhatsApp Bot",
        "tags": [
          "Internal WhatsApp Bot"
        ],
        "parameters": [
          {
            "name": "tenant_slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook diterima"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/platform/provisioning/readiness": {
      "get": {
        "summary": "Readiness checklist provisioning tenant",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "responses": {
          "200": {
            "description": "Readiness status"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/platform/provisioning/credentials": {
      "get": {
        "summary": "Lihat status credential provisioning",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "responses": {
          "200": {
            "description": "Credential status"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "put": {
        "summary": "Simpan credential provisioning terenkripsi",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantProvisioningCredentialRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Credential disimpan"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/platform/provisioning/credentials/test/{provider}": {
      "post": {
        "summary": "Test credential provisioning read-only",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "tenant_db",
                "cloudflare",
                "virtualmin",
                "storage",
                "backup"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Test berhasil"
          },
          "422": {
            "description": "Test gagal atau credential belum lengkap"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning": {
      "get": {
        "summary": "Lihat metadata provisioning tenant",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/preview": {
      "post": {
        "summary": "Preview provisioning tenant tanpa side effect",
        "description": "Tenant non-master default memakai mailbox root domain kliniktech.com, misalnya demo@kliniktech.com, sementara FQDN aplikasi tetap berbasis subdomain.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantProvisioningRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview dibuat"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/draft": {
      "post": {
        "summary": "Simpan draft provisioning tenant tanpa side effect",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantProvisioningRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft dibuat"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/subscriber-domain/provision": {
      "post": {
        "summary": "Provision DNS subscriber dan mailbox Virtualmin",
        "description": "Membuat/ensure DNS subscriber dan mailbox setelah frasa PROVISION SUBSCRIBER DOMAIN. Domain tenant aktif hanya bila DNS berhasil.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TenantProvisioningRequest"
                  },
                  {
                    "type": "object",
                    "required": [
                      "confirm_phrase",
                      "fqdn"
                    ],
                    "properties": {
                      "confirm_phrase": {
                        "type": "string",
                        "example": "PROVISION SUBSCRIBER DOMAIN"
                      },
                      "fqdn": {
                        "type": "string",
                        "example": "tenant.namaisp.id"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "DNS/mailbox diprovision"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/subscriber-domain/change-preview": {
      "post": {
        "summary": "Preview ganti domain subscriber",
        "description": "Mengembalikan domain aktif, preview domain baru, cek konflik tenant lain, status DNS custom, dan kandidat cleanup DNS lama.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantProvisioningRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview ganti domain dibuat"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/subscriber-domain/change-apply": {
      "post": {
        "summary": "Aktifkan domain baru subscriber",
        "description": "Membuat/ensure DNS baru dan menjadikannya domain aktif tenant setelah frasa CHANGE SUBSCRIBER DOMAIN. DNS lama tidak dihapus otomatis.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TenantProvisioningRequest"
                  },
                  {
                    "type": "object",
                    "required": [
                      "confirm_phrase",
                      "fqdn"
                    ],
                    "properties": {
                      "confirm_phrase": {
                        "type": "string",
                        "example": "CHANGE SUBSCRIBER DOMAIN"
                      },
                      "old_fqdn": {
                        "type": "string",
                        "nullable": true
                      },
                      "fqdn": {
                        "type": "string",
                        "example": "tenant.namaisp.id"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Domain baru aktif"
          },
          "409": {
            "description": "Domain aktif berubah atau domain lama masih primary"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/subscriber-domain/cleanup-old": {
      "post": {
        "summary": "Cleanup DNS lama subscriber",
        "description": "Menghapus record Cloudflare domain lama setelah domain baru verified memakai frasa DELETE OLD SUBSCRIBER DNS. Ditolak bila domain lama masih primary, bukan Cloudflare managed, atau record ID tidak tersimpan.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm_phrase",
                  "domain_id",
                  "fqdn"
                ],
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "DELETE OLD SUBSCRIBER DNS"
                  },
                  "domain_id": {
                    "type": "integer"
                  },
                  "fqdn": {
                    "type": "string",
                    "example": "tenant-lama.kliniktech.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "DNS lama dihapus"
          },
          "409": {
            "description": "Domain lama masih aktif/primary"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/pilot-db/preview": {
      "post": {
        "summary": "Preview DB-only pilot tenant",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantProvisioningPilotDbRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview DB-only pilot"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/pilot-db/run": {
      "post": {
        "summary": "Run DB-only pilot tenant dengan frasa konfirmasi",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantProvisioningPilotDbRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pilot DB-only selesai"
          },
          "422": {
            "description": "Pilot ditolak atau gagal"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/pilot-db/rollback": {
      "post": {
        "summary": "Rollback DB-only pilot tenant dengan frasa konfirmasi",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "ROLLBACK DB PILOT"
                  }
                },
                "required": [
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rollback DB-only pilot selesai"
          },
          "422": {
            "description": "Rollback ditolak atau gagal"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/test": {
      "post": {
        "summary": "Test dynamic tenant DB connection read-only",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tenant DB connection OK"
          },
          "422": {
            "description": "Tenant DB metadata missing or connection failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/read-sample": {
      "get": {
        "summary": "Read-sample shared DB vs tenant DB pilot",
        "description": "Membaca sample kecil users/customers/invoices/tickets dari shared DB dan tenant DB pilot untuk validasi readable tanpa cutover production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read-sample completed"
          },
          "422": {
            "description": "Read-sample rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/users": {
      "get": {
        "summary": "Pilot read-only users from tenant DB",
        "description": "Membaca users dari tenant DB hanya jika feature flag use_tenant_db_read aktif dan metadata tenant DB schema_migrated. Endpoint ini belum cutover login atau modul production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Users read completed"
          },
          "422": {
            "description": "Users read rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/customers": {
      "get": {
        "summary": "Pilot read-only customers from tenant DB",
        "description": "Membaca customers dari tenant DB hanya jika feature flag use_tenant_db_read aktif dan metadata tenant DB schema_migrated. Endpoint ini belum cutover modul pelanggan production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Customers read completed"
          },
          "422": {
            "description": "Customers read rejected or failed"
          }
        }
      }
    },
    "/tenant-db-pilot/tenants/{tenant}/customers": {
      "get": {
        "summary": "Pilot-token read-only customers from tenant DB",
        "description": "Smoke route untuk token dengan ability pilot:tenant-db-login dan tenant:{id}. Membaca customers dari tenant DB pilot tanpa akses Platform Admin dan belum cutover modul production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "security": [
          {
            "sanctum": []
          }
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Customers read completed"
          },
          "403": {
            "description": "Pilot token ability rejected"
          },
          "422": {
            "description": "Customers read rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/invoices": {
      "get": {
        "summary": "Pilot read-only invoices from tenant DB",
        "description": "Membaca invoices dari tenant DB pilot. Belum cutover modul Billing production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invoices read completed"
          },
          "422": {
            "description": "Invoices read rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/tickets": {
      "get": {
        "summary": "Pilot read-only tickets from tenant DB",
        "description": "Membaca tickets dari tenant DB pilot. Belum cutover modul Ticket production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tickets read completed"
          },
          "422": {
            "description": "Tickets read rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/demo-flow": {
      "get": {
        "summary": "Pilot read-only demo flow from tenant DB",
        "description": "Membaca dan memvalidasi relasi DEMO-CUST-001 ke DEMO-INV-001 dan DEMO-TICK-001 dari tenant DB pilot. Tidak menulis data dan belum cutover production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Demo flow complete"
          },
          "422": {
            "description": "Demo flow incomplete or rejected"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/pilot-health": {
      "get": {
        "summary": "Tenant DB pilot health check",
        "description": "Read-only health check untuk tenant DB pilot: flag use_tenant_db_read, metadata/schema, koneksi DB, count tabel minimal, demo flow, dan token pilot aktif.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "pilot_email",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "email",
              "default": "demo@kliniktech.com"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pilot health is ready"
          },
          "422": {
            "description": "Pilot health is incomplete or rejected"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/shadow-read": {
      "get": {
        "summary": "Tenant DB shadow-read parity",
        "description": "Read-only parity check shared DB vs tenant DB pilot untuk users, customers, invoices, dan tickets. Tidak mengubah routing production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Shadow-read completed"
          },
          "422": {
            "description": "Shadow-read rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/reconciliation": {
      "get": {
        "summary": "Tenant DB reconciliation report",
        "description": "Read-only reconciliation report shared DB vs tenant DB berdasarkan shadow-read, dengan severity per tabel. Extra row pilot DEMO-* yang valid dihitung sebagai accepted_pilot_extra; extra lain tetap blocking_extra_key_count.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reconciliation completed"
          },
          "422": {
            "description": "Reconciliation rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/cutover-gate": {
      "get": {
        "summary": "Tenant DB cutover gate report",
        "description": "Read-only gate report untuk mengevaluasi pilot health, reconciliation, write-plan, dan block sebelum production cutover. Tidak menjalankan cutover; gate hijau pilot tetap membutuhkan approval production eksplisit.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cutover gate evaluated"
          },
          "422": {
            "description": "Cutover gate rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/readiness-snapshot": {
      "get": {
        "summary": "Tenant DB readiness snapshot",
        "description": "Read-only evidence snapshot HA/MTDB yang menggabungkan pilot health, reconciliation, write-plan, dan cutover gate. Tidak menjalankan cutover; production_cutover.allowed tetap false sampai approval production eksplisit. Response menyertakan evidence.fingerprint SHA-256, evidence.fingerprint_command, evidence.summary_command, evidence.status_command, evidence.commands_command, evidence.verify_command, evidence.verify_summary_command, evidence.verify_status_command, evidence.decision_command, dan evidence.operator_action_command untuk dicatat sebagai ID bukti approval serta command read-only. Tenant master kliniktech dijaga sebagai shared_db_master dan tidak menjalankan checker tenant DB.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Readiness snapshot generated"
          },
          "422": {
            "description": "Readiness snapshot rejected or blocked"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/readiness-snapshot/verify": {
      "get": {
        "summary": "Verify Tenant DB readiness evidence fingerprint",
        "description": "Read-only verification untuk membandingkan evidence.fingerprint yang dicatat dengan readiness snapshot terbaru. Response mengembalikan matched=true/false, decision, dan operator_action; tetap tidak menjalankan cutover, dan production_cutover_allowed tetap false.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "fingerprint",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-fA-F0-9]{64}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fingerprint comparison completed"
          },
          "422": {
            "description": "Fingerprint validation failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/production-status": {
      "get": {
        "summary": "Tenant DB production cutover status",
        "description": "Read-only status post-cutover tenant DB: cutover status, approval, module routing, feature flags, dan rollback readiness. Tidak menulis perubahan.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Production cutover status is active and module routing is tenant_db"
          },
          "422": {
            "description": "Production cutover status is not active or routing is not complete"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/approve-production": {
      "post": {
        "summary": "Approve Tenant DB production cutover",
        "description": "Mencatat approval production setelah readiness fingerprint valid. Wajib fingerprint, approver, maintenance_window, rollback_owner, monitoring_owner, reason, dan frasa APPROVE TENANT DB PRODUCTION. Tidak menjalankan cutover.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "fingerprint",
                  "approver",
                  "maintenance_window",
                  "rollback_owner",
                  "monitoring_owner",
                  "reason",
                  "confirm_phrase"
                ],
                "properties": {
                  "fingerprint": {
                    "type": "string",
                    "pattern": "^[a-fA-F0-9]{64}$"
                  },
                  "approver": {
                    "type": "string"
                  },
                  "maintenance_window": {
                    "type": "string"
                  },
                  "rollback_owner": {
                    "type": "string"
                  },
                  "monitoring_owner": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "confirm_phrase": {
                    "type": "string",
                    "example": "APPROVE TENANT DB PRODUCTION"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Production approval recorded"
          },
          "422": {
            "description": "Readiness fingerprint, metadata, or confirmation phrase invalid"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/cutover-production": {
      "post": {
        "summary": "Cutover Tenant DB production",
        "description": "Menjalankan guarded production cutover setelah fingerprint dan approval ID valid. Wajib frasa CUTOVER TENANT DB PRODUCTION. Tenant master kliniktech tetap diblokir.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "fingerprint",
                  "approval_id",
                  "confirm_phrase"
                ],
                "properties": {
                  "fingerprint": {
                    "type": "string",
                    "pattern": "^[a-fA-F0-9]{64}$"
                  },
                  "approval_id": {
                    "type": "string"
                  },
                  "confirm_phrase": {
                    "type": "string",
                    "example": "CUTOVER TENANT DB PRODUCTION"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Production cutover completed"
          },
          "422": {
            "description": "Cutover blocked by readiness, approval, or confirmation guard"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/rollback-production": {
      "post": {
        "summary": "Rollback Tenant DB production cutover",
        "description": "Rollback guarded untuk production cutover. dry_run=true tidak menulis perubahan; rollback aktual wajib frasa ROLLBACK TENANT DB PRODUCTION.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "approval_id"
                ],
                "properties": {
                  "approval_id": {
                    "type": "string"
                  },
                  "dry_run": {
                    "type": "boolean",
                    "default": true
                  },
                  "confirm_phrase": {
                    "type": "string",
                    "example": "ROLLBACK TENANT DB PRODUCTION"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rollback dry-run or production rollback completed"
          },
          "422": {
            "description": "Rollback blocked by approval, state, or confirmation guard"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/module-read/{module}": {
      "get": {
        "summary": "Controlled tenant DB module read",
        "description": "Smoke test resolver source modul untuk users/customers/invoices/tickets. Default shared DB; tenant DB hanya dipilih jika flag granular modul aktif dan schema siap.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "module",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "users",
                "customers",
                "invoices",
                "tickets"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "key",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Module read completed"
          },
          "422": {
            "description": "Module read rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/write-plan": {
      "get": {
        "summary": "Tenant DB write-path dry-run planner",
        "description": "Read-only planner untuk jalur write customers/invoices/tickets sebelum dual-write pilot. Tidak menulis data. Jalur komentar/lampiran tiket siap hanya jika schema tenant memiliki ticket_comments dan ticket_attachments.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Write plan generated"
          },
          "422": {
            "description": "Write plan rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/dual-write/run": {
      "post": {
        "summary": "Run limited tenant DB dual-write pilot",
        "description": "Guarded pilot write untuk row demo DEMO-* di tenant DB. Tidak mengubah routing production. Wajib frasa RUN TENANT DB DUAL WRITE PILOT.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "RUN TENANT DB DUAL WRITE PILOT"
                  }
                },
                "required": [
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dual-write pilot completed"
          },
          "422": {
            "description": "Dual-write pilot rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/dual-write/rollback": {
      "post": {
        "summary": "Rollback limited tenant DB dual-write pilot",
        "description": "Mengembalikan snapshot row demo DEMO-* dari tenant DB pilot. Wajib frasa ROLLBACK TENANT DB DUAL WRITE PILOT.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "ROLLBACK TENANT DB DUAL WRITE PILOT"
                  }
                },
                "required": [
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dual-write rollback completed"
          },
          "422": {
            "description": "Dual-write rollback rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/demo-seed": {
      "post": {
        "summary": "Seed tenant DB pilot demo data",
        "description": "Membuat data demo kecil customer/invoice/ticket di tenant DB pilot. Hanya untuk tenant demo/pilot dan wajib frasa SEED TENANT DB DEMO DATA.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "SEED TENANT DB DEMO DATA"
                  }
                },
                "required": [
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Demo data seeded"
          },
          "422": {
            "description": "Seed rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/demo-seed/rollback": {
      "post": {
        "summary": "Rollback tenant DB pilot demo data",
        "description": "Menghapus hanya row demo DEMO-* dari tenant DB pilot. Wajib frasa ROLLBACK TENANT DB DEMO DATA.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "ROLLBACK TENANT DB DEMO DATA"
                  }
                },
                "required": [
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Demo data rolled back"
          },
          "422": {
            "description": "Rollback rejected or failed"
          }
        }
      }
    },
    "/tenant-db-pilot/tenants/{tenant}/invoices": {
      "get": {
        "summary": "Pilot-token read-only invoices from tenant DB",
        "description": "Smoke route invoices untuk token dengan ability pilot:tenant-db-login dan tenant:{id}.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "security": [
          {
            "sanctum": []
          }
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invoices read completed"
          },
          "403": {
            "description": "Pilot token ability rejected"
          },
          "422": {
            "description": "Invoices read rejected or failed"
          }
        }
      }
    },
    "/tenant-db-pilot/tenants/{tenant}/tickets": {
      "get": {
        "summary": "Pilot-token read-only tickets from tenant DB",
        "description": "Smoke route tickets untuk token dengan ability pilot:tenant-db-login dan tenant:{id}.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "security": [
          {
            "sanctum": []
          }
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tickets read completed"
          },
          "403": {
            "description": "Pilot token ability rejected"
          },
          "422": {
            "description": "Tickets read rejected or failed"
          }
        }
      }
    },
    "/tenant-db-pilot/tenants/{tenant}/demo-flow": {
      "get": {
        "summary": "Pilot-token read-only demo flow from tenant DB",
        "description": "Smoke route demo flow untuk token dengan ability pilot:tenant-db-login dan tenant:{id}.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "security": [
          {
            "sanctum": []
          }
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Demo flow complete"
          },
          "403": {
            "description": "Pilot token ability rejected"
          },
          "422": {
            "description": "Demo flow incomplete or rejected"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/auth-check": {
      "post": {
        "summary": "Pilot tenant DB auth-check read-only",
        "description": "Memverifikasi email/password terhadap hash user di tenant DB pilot tanpa membuat token, tanpa session, tanpa update last_login, dan tanpa mengganti /auth/login production.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "demo@kliniktech.com"
                  },
                  "password": {
                    "type": "string",
                    "format": "password"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Auth-check valid"
          },
          "422": {
            "description": "Auth-check rejected or invalid"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/pilot-login": {
      "post": {
        "summary": "Internal pilot login using tenant DB",
        "description": "Memvalidasi password dari tenant DB lalu membuat token Sanctum pilot untuk user pusat yang cocok. Tidak mengubah /auth/login production, tidak menghapus token existing, tidak update last_login, dan wajib frasa PILOT TENANT DB LOGIN.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "demo@kliniktech.com"
                  },
                  "password": {
                    "type": "string",
                    "format": "password"
                  },
                  "confirm_phrase": {
                    "type": "string",
                    "example": "PILOT TENANT DB LOGIN"
                  }
                },
                "required": [
                  "email",
                  "password",
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pilot login token created"
          },
          "422": {
            "description": "Pilot login rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/pilot-token/status": {
      "get": {
        "summary": "Pilot token status",
        "description": "Melihat status token pilot tenant DB untuk email tertentu. Read-only.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "email",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pilot token status"
          },
          "422": {
            "description": "Status rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-db/pilot-token/revoke": {
      "post": {
        "summary": "Revoke pilot tenant DB token",
        "description": "Menghapus hanya token tenant-db-pilot-token dengan ability pilot:tenant-db-login setelah frasa REVOKE TENANT DB PILOT TOKEN.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "demo@kliniktech.com"
                  },
                  "confirm_phrase": {
                    "type": "string",
                    "example": "REVOKE TENANT DB PILOT TOKEN"
                  }
                },
                "required": [
                  "email",
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pilot tokens revoked"
          },
          "422": {
            "description": "Revoke rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-schema/preview": {
      "get": {
        "summary": "Preview tenant pilot schema migration",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tenant schema preview"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/tenant-schema/run": {
      "post": {
        "summary": "Run tenant pilot schema migration",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "RUN TENANT SCHEMA"
                  }
                },
                "required": [
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tenant schema migration completed"
          },
          "422": {
            "description": "Migration rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/data-copy/dry-run": {
      "post": {
        "summary": "Dry-run copy minimal tenant data",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Dry-run completed without writing tenant data"
          },
          "422": {
            "description": "Dry-run rejected or tenant database metadata missing"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/data-copy/run": {
      "post": {
        "summary": "Run pilot copy minimal tenant data",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "COPY TENANT DATA PILOT"
                  }
                },
                "required": [
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pilot data copy completed"
          },
          "422": {
            "description": "Copy rejected or failed"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/data-copy/rollback": {
      "post": {
        "summary": "Rollback pilot copied tenant data",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_phrase": {
                    "type": "string",
                    "example": "ROLLBACK TENANT DATA PILOT"
                  }
                },
                "required": [
                  "confirm_phrase"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pilot data rollback completed"
          },
          "422": {
            "description": "Rollback rejected or failed"
          }
        }
      }
    },
    "/file-manager": {
      "get": {
        "summary": "List File Manager tenant",
        "tags": [
          "Internal File Manager"
        ],
        "parameters": [
          {
            "name": "tenant_id",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Opsional untuk superadmin atau tenant master"
          },
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "private",
                "shared"
              ]
            }
          },
          {
            "name": "folder",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "owner_user_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "$ref": "#/components/parameters/Search"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "summary": "Upload file ke File Manager tenant",
        "tags": [
          "Internal File Manager"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "tenant_id": {
                    "type": "integer"
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "private",
                      "shared"
                    ]
                  },
                  "folder": {
                    "type": "string",
                    "example": "dokumen"
                  },
                  "owner_user_id": {
                    "type": "integer"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File diupload"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/file-manager/{id}/download": {
      "get": {
        "summary": "Download file manager tenant",
        "tags": [
          "Internal File Manager"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File binary"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/file-manager/{id}": {
      "delete": {
        "summary": "Hapus file manager tenant",
        "tags": [
          "Internal File Manager"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "File dihapus"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/external/v1/health": {
      "get": {
        "summary": "Cek status API dan token",
        "tags": [
          "System"
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/external/v1/me": {
      "get": {
        "summary": "Info tenant, user, dan ability token",
        "tags": [
          "System"
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/external/v1/customers": {
      "get": {
        "summary": "List pelanggan",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "$ref": "#/components/parameters/Search"
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "summary": "Buat pelanggan",
        "tags": [
          "Customers"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pelanggan dibuat"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/external/v1/customers/{id}": {
      "get": {
        "summary": "Detail pelanggan",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/external/v1/packages": {
      "get": {
        "summary": "List paket layanan",
        "tags": [
          "Packages"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Search"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "name": "is_active",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/external/v1/invoices": {
      "get": {
        "summary": "List invoice",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Search"
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customer_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/external/v1/invoices/{id}": {
      "get": {
        "summary": "Detail invoice",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/external/v1/tickets": {
      "get": {
        "summary": "List tiket",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Search"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "priority",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customer_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "summary": "Buat tiket",
        "tags": [
          "Tickets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TicketCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tiket dibuat"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/external/v1/tickets/{id}": {
      "get": {
        "summary": "Detail tiket",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/portal/tickets/{id}/reopen": {
      "post": {
        "summary": "Reopen tiket dari portal pelanggan",
        "description": "Pelanggan membuka kembali tiket miliknya yang sudah resolved/closed. Endpoint hanya berhasil jika opsi pelanggan portal diaktifkan pada policy reopen tiket tenant.",
        "tags": [
          "Portal"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 2000,
                    "example": "Internet masih putus setelah tiket dinyatakan selesai."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tiket berhasil dibuka kembali"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/external/v1/olts": {
      "get": {
        "summary": "List OLT",
        "tags": [
          "Network"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Search"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/external/v1/onus": {
      "get": {
        "summary": "List ONU aktif",
        "tags": [
          "Network"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Search"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customer_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "olt_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/suppliers/{supplier}/items/import-preview": {
      "post": {
        "summary": "Preview import barang yang dijual supplier",
        "tags": [
          "Internal Inventory Supplier"
        ],
        "parameters": [
          {
            "name": "supplier",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "CSV, TXT, XLSX, atau PDF maksimal 10 MB"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview berhasil"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/suppliers/{supplier}/items/import": {
      "post": {
        "summary": "Import barang supplier dari hasil preview",
        "tags": [
          "Internal Inventory Supplier"
        ],
        "parameters": [
          {
            "name": "supplier",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/SupplierImportItem"
                    }
                  }
                },
                "required": [
                  "items"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import selesai"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/network/vyos-dhcp-leases": {
      "get": {
        "tags": [
          "Network / VyOS"
        ],
        "summary": "Daftar DHCP lease VyOS",
        "description": "Mengambil DHCP lease dari NAS VyOS dan menandai apakah lease sudah terhubung ke pelanggan. Lease tanpa customer_id dapat dipakai untuk pending binding pelanggan DHCP/Static.",
        "parameters": [
          {
            "name": "router_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Filter NAS VyOS tertentu."
          },
          {
            "name": "refresh",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Paksa refresh dari VyOS, abaikan cache singkat."
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar lease DHCP VyOS.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "dhcp": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "address": {
                            "type": "string",
                            "example": "10.10.20.15"
                          },
                          "mac_address": {
                            "type": "string",
                            "example": "aa:bb:cc:dd:ee:ff"
                          },
                          "hostname": {
                            "type": "string",
                            "nullable": true
                          },
                          "customer_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "customer": {
                            "type": "string",
                            "nullable": true
                          },
                          "bound": {
                            "type": "boolean"
                          },
                          "binding_status": {
                            "type": "string",
                            "example": "unbound"
                          },
                          "action_hint": {
                            "type": "string"
                          },
                          "router_id": {
                            "type": "integer",
                            "nullable": true
                          },
                          "router_name": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/network/vyos-dhcp-leases/bind": {
      "post": {
        "tags": [
          "Network / VyOS"
        ],
        "summary": "Hubungkan DHCP lease VyOS ke pelanggan",
        "description": "Dipakai saat operator tidak tahu MAC ONU/CPE ketika input pelanggan. Pelanggan boleh dibuat tanpa MAC, lalu lease VyOS yang sudah online dihubungkan ke customer DHCP/Static.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "customer_id",
                  "mac_address",
                  "address"
                ],
                "properties": {
                  "customer_id": {
                    "type": "integer",
                    "example": 123
                  },
                  "mac_address": {
                    "type": "string",
                    "example": "aa:bb:cc:dd:ee:ff"
                  },
                  "address": {
                    "type": "string",
                    "example": "10.10.20.15"
                  },
                  "router_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 2
                  },
                  "sync": {
                    "type": "boolean",
                    "default": true,
                    "description": "Jika true, provisioning normal atau isolir address-group langsung dijalankan."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lease berhasil dihubungkan ke pelanggan."
          },
          "422": {
            "description": "Binding ditolak dengan pesan jelas, misalnya pelanggan bukan DHCP/Static, MAC sudah dipakai pelanggan lain, lease tidak punya IP valid, atau NAS VyOS tidak aktif."
          },
          "403": {
            "description": "User tidak memiliki permission customers.network."
          }
        }
      }
    },
    "/attendance/dashboard": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Ringkasan absensi, work log aktif, dan lembur periode berjalan.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "latitude": {
                    "type": "number",
                    "format": "float",
                    "example": -8.650123
                  },
                  "longitude": {
                    "type": "number",
                    "format": "float",
                    "example": 115.216456
                  },
                  "accuracy": {
                    "type": "number",
                    "format": "float",
                    "description": "Akurasi GPS dari browser dalam meter. Default tenant menolak nilai di atas 50 meter.",
                    "example": 18
                  },
                  "selfie": {
                    "type": "string",
                    "description": "Data URL base64 JPG/PNG/WEBP maksimal 5 MB.",
                    "example": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 1000,
                    "example": "Mulai shift pagi"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          },
          "422": {
            "description": "GPS/selfie tidak valid, di luar radius, atau sudah check-in"
          },
          "503": {
            "description": "Schema absensi belum tersedia"
          }
        }
      }
    },
    "/attendance": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Daftar absensi karyawan per tenant.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "latitude": {
                    "type": "number",
                    "format": "float",
                    "example": -8.650123
                  },
                  "longitude": {
                    "type": "number",
                    "format": "float",
                    "example": 115.216456
                  },
                  "accuracy": {
                    "type": "number",
                    "format": "float",
                    "description": "Akurasi GPS dari browser dalam meter. Default tenant menolak nilai di atas 50 meter.",
                    "example": 18
                  },
                  "selfie": {
                    "type": "string",
                    "description": "Data URL base64 JPG/PNG/WEBP maksimal 5 MB.",
                    "example": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 1000,
                    "example": "Pulang setelah closing tiket"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          },
          "422": {
            "description": "GPS/selfie tidak valid, di luar radius, atau belum check-in"
          },
          "503": {
            "description": "Schema absensi belum tersedia"
          }
        }
      }
    },
    "/attendance/clock-in": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Check-in absensi karyawan.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          },
          "422": {
            "description": "Validasi gagal, termasuk jika selfie_source bukan camera atau foto bukan dari kamera langsung."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "latitude",
                  "longitude",
                  "selfie",
                  "selfie_source"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Opsional untuk admin/koordinator saat absensi staff lain."
                  },
                  "latitude": {
                    "type": "number",
                    "format": "float",
                    "example": -8.650123
                  },
                  "longitude": {
                    "type": "number",
                    "format": "float",
                    "example": 115.216456
                  },
                  "accuracy": {
                    "type": "number",
                    "format": "float",
                    "example": 12
                  },
                  "selfie": {
                    "type": "string",
                    "description": "Data URL base64 foto selfie dari kamera langsung perangkat."
                  },
                  "selfie_source": {
                    "type": "string",
                    "enum": [
                      "camera"
                    ],
                    "description": "Wajib bernilai camera. Check-in dari upload galeri/file ditolak."
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true,
                    "example": "Mulai shift pagi"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attendance/clock-out": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Check-out absensi karyawan.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          }
        }
      }
    },
    "/attendance/work-logs": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Daftar work log teknisi dari jadwal/tiket.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          }
        }
      }
    },
    "/attendance/overtime": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Daftar pengajuan lembur.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          }
        }
      }
    },
    "/attendance/overtime/{approval}/approve": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Setujui menit lembur dan hitung nominal payroll.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          }
        },
        "parameters": [
          {
            "name": "approval",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/attendance/overtime/{approval}/reject": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Tolak pengajuan lembur dengan alasan.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          }
        },
        "parameters": [
          {
            "name": "approval",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ]
      }
    },
    "/attendance/settings": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Baca pengaturan jam kerja dan lembur tenant.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          }
        },
        "description": "Pengaturan Absensi termasuk GPS, selfie, trusted device, anti mock GPS, skor fraud, liveness wajah wajib, dan skor minimal liveness. Mendukung branding/theme /absensi: attendance_portal_theme_mode, attendance_portal_brand_title, attendance_portal_brand_subtitle, attendance_portal_hero_title, attendance_portal_hero_subtitle, attendance_portal_accent_color, attendance_portal_hero_color, attendance_portal_show_illustration."
      },
      "put": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Simpan pengaturan jam kerja, lembur, GPS, selfie, dan radius absensi tenant.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "attendance_require_location": {
                    "type": "boolean",
                    "example": true
                  },
                  "attendance_require_selfie": {
                    "type": "boolean",
                    "example": true
                  },
                  "attendance_office_latitude": {
                    "type": "number",
                    "format": "float",
                    "nullable": true,
                    "example": -8.650123
                  },
                  "attendance_office_longitude": {
                    "type": "number",
                    "format": "float",
                    "nullable": true,
                    "example": 115.216456
                  },
                  "attendance_max_distance_m": {
                    "type": "integer",
                    "example": 300
                  },
                  "attendance_max_accuracy_m": {
                    "type": "integer",
                    "description": "Batas akurasi GPS maksimal dalam meter. Isi 0 untuk tidak membatasi akurasi.",
                    "example": 50
                  },
                  "attendance_annual_leave_days": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 365,
                    "example": 12
                  },
                  "attendance_approval_notify_wa": {
                    "type": "boolean",
                    "example": true
                  },
                  "attendance_approval_notify_telegram": {
                    "type": "boolean",
                    "example": true
                  },
                  "attendance_approval_wa_numbers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "+62811385690"
                    ]
                  },
                  "attendance_approval_telegram_chat_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "7945837844"
                    ]
                  },
                  "attendance_require_liveness": {
                    "type": "boolean",
                    "description": "Jika true, check-in mobile wajib liveness wajah."
                  },
                  "attendance_liveness_min_score": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Skor minimal liveness agar check-in diterima."
                  },
                  "attendance_portal_theme_mode": {
                    "type": "string",
                    "enum": [
                      "user",
                      "dark",
                      "light"
                    ],
                    "description": "Mode theme default /absensi. user berarti mengikuti pilihan user di perangkat."
                  },
                  "attendance_portal_brand_title": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "attendance_portal_brand_subtitle": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "attendance_portal_hero_title": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "attendance_portal_hero_subtitle": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "attendance_portal_accent_color": {
                    "type": "string",
                    "pattern": "^#[0-9A-Fa-f]{6}$",
                    "example": "#38bdf8"
                  },
                  "attendance_portal_hero_color": {
                    "type": "string",
                    "pattern": "^#[0-9A-Fa-f]{6}$",
                    "example": "#2563eb"
                  },
                  "attendance_portal_show_illustration": {
                    "type": "boolean"
                  },
                  "attendance_tracking_enabled": {
                    "type": "boolean"
                  },
                  "attendance_tracking_interval_seconds": {
                    "type": "integer",
                    "minimum": 30,
                    "maximum": 1800
                  },
                  "attendance_tracking_max_accuracy_m": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10000
                  },
                  "attendance_tracking_stale_minutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 240
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          }
        },
        "description": "Pengaturan Absensi termasuk GPS, selfie, trusted device, anti mock GPS, skor fraud, liveness wajah wajib, dan skor minimal liveness. Mendukung branding/theme /absensi: attendance_portal_theme_mode, attendance_portal_brand_title, attendance_portal_brand_subtitle, attendance_portal_hero_title, attendance_portal_hero_subtitle, attendance_portal_accent_color, attendance_portal_hero_color, attendance_portal_show_illustration."
      }
    },
    "/attendance/export": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Export rekap lembur CSV.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          }
        }
      }
    },
    "/attendance/requests": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Daftar pengajuan izin, cuti, sakit, dan koreksi absensi.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          },
          "503": {
            "description": "Schema attendance_requests belum tersedia"
          }
        }
      },
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Buat pengajuan izin/cuti/sakit atau koreksi absensi.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "request_type",
                  "reason"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 27
                  },
                  "request_type": {
                    "type": "string",
                    "enum": [
                      "leave",
                      "sick",
                      "permission",
                      "correction"
                    ],
                    "example": "permission"
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date",
                    "example": "2026-06-24"
                  },
                  "end_date": {
                    "type": "string",
                    "format": "date",
                    "example": "2026-06-24"
                  },
                  "work_date": {
                    "type": "string",
                    "format": "date",
                    "example": "2026-06-24"
                  },
                  "requested_check_in_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "example": "2026-06-24T08:00:00+07:00"
                  },
                  "requested_check_out_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "example": "2026-06-24T17:00:00+07:00"
                  },
                  "reason": {
                    "type": "string",
                    "minLength": 5,
                    "maxLength": 2000,
                    "example": "Izin keluarga atau koreksi karena lupa check-out."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pengajuan dibuat"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          },
          "422": {
            "description": "Validasi pengajuan gagal"
          },
          "503": {
            "description": "Schema attendance_requests belum tersedia"
          }
        }
      }
    },
    "/attendance/requests/{attendanceRequest}/approve": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Setujui pengajuan Absensi dan terapkan ke data attendance.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "attendanceRequest",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notes": {
                    "type": "string",
                    "maxLength": 1000,
                    "example": "Disetujui oleh koordinator."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          },
          "404": {
            "description": "Pengajuan tidak ditemukan"
          },
          "422": {
            "description": "Pengajuan sudah diproses atau alasan tidak valid"
          },
          "503": {
            "description": "Schema attendance_requests belum tersedia"
          }
        }
      }
    },
    "/attendance/requests/{attendanceRequest}/reject": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Tolak pengajuan Absensi dengan alasan wajib.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "attendanceRequest",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rejection_reason": {
                    "type": "string",
                    "minLength": 5,
                    "maxLength": 1000,
                    "example": "Bukti belum lengkap."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "401": {
            "description": "Token tidak valid"
          },
          "403": {
            "description": "Permission tidak cukup"
          },
          "404": {
            "description": "Pengajuan tidak ditemukan"
          },
          "422": {
            "description": "Pengajuan sudah diproses atau alasan tidak valid"
          },
          "503": {
            "description": "Schema attendance_requests belum tersedia"
          }
        }
      }
    },
    "/attendance/leave-balances": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Daftar kuota cuti tahunan per karyawan.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "schema": {
              "type": "integer",
              "example": 2026
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "503": {
            "description": "Schema kuota/audit belum tersedia"
          }
        }
      },
      "put": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Ubah kuota cuti tahunan karyawan.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id",
                  "year",
                  "entitlement_days"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "example": 27
                  },
                  "year": {
                    "type": "integer",
                    "example": 2026
                  },
                  "entitlement_days": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 365,
                    "example": 12
                  },
                  "adjustment_days": {
                    "type": "integer",
                    "minimum": -365,
                    "maximum": 365,
                    "example": 0
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 1000,
                    "example": "Carry-over 2 hari dari tahun lalu."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kuota diperbarui"
          },
          "422": {
            "description": "Validasi gagal"
          },
          "503": {
            "description": "Schema kuota/audit belum tersedia"
          }
        }
      }
    },
    "/attendance/payroll-report": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Laporan payroll Absensi periode.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          }
        },
        "description": "Mengembalikan/mengunduh rekap payroll dengan gaji pokok, tunjangan, potongan, lembur, dan Take Home Pay berdasarkan policy staff. Field payroll mencakup late_minutes, late_hours, late_deduction_mode, late_as_absent_days, absence_deduction, dan other_deduction_items."
      }
    },
    "/attendance/audit-logs": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Audit trail perubahan Absensi.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "event",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "request_approved"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "503": {
            "description": "Schema kuota/audit belum tersedia"
          }
        }
      }
    },
    "/attendance/export-payroll": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Export CSV payroll Absensi.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV payroll Absensi",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "description": "Mengembalikan/mengunduh rekap payroll dengan gaji pokok, tunjangan, potongan, lembur, dan Take Home Pay berdasarkan policy staff."
      }
    },
    "/attendance/staff-policies": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Daftar policy Absensi dan Payroll per staff.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Berhasil"
          },
          "503": {
            "description": "Schema attendance_staff_policies belum tersedia"
          }
        }
      },
      "put": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Simpan policy multi Home Base, jadwal fleksibel, dan payroll staff.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id"
                ],
                "properties": {
                  "user_id": {
                    "type": "integer",
                    "example": 27
                  },
                  "primary_home_base_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 2
                  },
                  "home_base_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "example": [
                      2,
                      3
                    ]
                  },
                  "allow_any_home_base": {
                    "type": "boolean",
                    "example": false
                  },
                  "flexible_schedule": {
                    "type": "boolean",
                    "example": true
                  },
                  "work_days": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 7
                    },
                    "example": [
                      1,
                      2,
                      3,
                      4,
                      5,
                      6
                    ]
                  },
                  "work_start": {
                    "type": "string",
                    "example": "08:00"
                  },
                  "work_end": {
                    "type": "string",
                    "example": "17:00"
                  },
                  "grace_minutes": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 240,
                    "example": 15
                  },
                  "base_salary": {
                    "type": "integer",
                    "example": 3000000
                  },
                  "fixed_allowance": {
                    "type": "integer",
                    "example": 250000
                  },
                  "meal_allowance_per_day": {
                    "type": "integer",
                    "example": 25000
                  },
                  "transport_allowance_per_day": {
                    "type": "integer",
                    "example": 15000
                  },
                  "late_deduction_per_day": {
                    "type": "integer",
                    "example": 10000
                  },
                  "late_deduction_mode": {
                    "type": "string",
                    "enum": [
                      "daily",
                      "hourly",
                      "minute"
                    ],
                    "example": "hourly",
                    "description": "Mode nominal potongan telat: daily = per hari, hourly = per jam dibulatkan ke atas, minute = per menit aktual."
                  },
                  "late_absent_after_minutes": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1440,
                    "example": 180,
                    "description": "Jika lebih dari 0, telat mencapai batas ini dihitung sebagai tidak masuk pada payroll."
                  },
                  "absence_deduction_per_day": {
                    "type": "integer",
                    "example": 100000
                  },
                  "other_deduction": {
                    "type": "integer",
                    "example": 0
                  },
                  "other_deduction_items": {
                    "type": "array",
                    "description": "Daftar kategori potongan lain per periode payroll. Totalnya disimpan kompatibel ke other_deduction.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "category": {
                          "type": "string",
                          "enum": [
                            "bpjs",
                            "asuransi",
                            "kasbon",
                            "koperasi",
                            "lainnya"
                          ],
                          "example": "bpjs"
                        },
                        "label": {
                          "type": "string",
                          "example": "BPJS Kesehatan"
                        },
                        "amount": {
                          "type": "integer",
                          "minimum": 1,
                          "example": 75000
                        }
                      }
                    }
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 1000,
                    "example": "Staff fleksibel lintas home base."
                  },
                  "is_attendance_staff": {
                    "type": "boolean",
                    "example": true,
                    "description": "Jika false, user tetap bisa login sesuai role tetapi tidak dihitung Absensi/Payroll dan tidak boleh check-in/check-out."
                  },
                  "employment_status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "inactive",
                      "contract",
                      "freelance",
                      "part_time"
                    ],
                    "example": "active"
                  },
                  "payroll_enabled": {
                    "type": "boolean",
                    "example": true,
                    "description": "Jika false, staff tetap bisa ikut Absensi tetapi tidak dihitung dalam Payroll/THP."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Policy disimpan"
          },
          "422": {
            "description": "Validasi policy gagal"
          },
          "503": {
            "description": "Schema attendance_staff_policies belum tersedia"
          }
        }
      }
    },
    "/inventory/requests/pending-count": {
      "get": {
        "summary": "Jumlah Request Barang pending untuk badge NetAdmin",
        "tags": [
          "Inventory"
        ],
        "responses": {
          "200": {
            "description": "Objek { count: number }"
          }
        }
      }
    },
    "/inventory/requests": {
      "get": {
        "summary": "List Request Barang multi-item",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar request barang"
          },
          "503": {
            "description": "Tabel inventory_requests belum tersedia"
          }
        }
      },
      "post": {
        "summary": "Buat Request Barang multi-item",
        "tags": [
          "Inventory"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "items"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "out",
                      "transfer",
                      "return"
                    ],
                    "example": "out"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "pending"
                    ],
                    "example": "pending"
                  },
                  "source_location_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 1
                  },
                  "destination_location_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 2
                  },
                  "reference_type": {
                    "type": "string",
                    "enum": [
                      "ticket",
                      "customer",
                      "schedule",
                      "manual"
                    ],
                    "nullable": true,
                    "example": "ticket"
                  },
                  "reference_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 123
                  },
                  "reason": {
                    "type": "string",
                    "nullable": true,
                    "example": "pasang"
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true,
                    "example": "Material PSB pelanggan"
                  },
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": [
                        "item_id",
                        "quantity"
                      ],
                      "properties": {
                        "item_id": {
                          "type": "integer",
                          "example": 12
                        },
                        "quantity": {
                          "type": "number",
                          "example": 2
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "ONU + adaptor"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft dibuat"
          },
          "202": {
            "description": "Request dikirim"
          },
          "422": {
            "description": "Validasi gagal"
          },
          "503": {
            "description": "Tabel inventory_requests belum tersedia"
          }
        }
      }
    },
    "/inventory/requests/{id}": {
      "get": {
        "summary": "Detail Request Barang",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Detail request barang"
          }
        }
      },
      "put": {
        "summary": "Edit Request Barang draft/pending/ditangguhkan",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "items"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "out",
                      "transfer",
                      "return"
                    ],
                    "example": "out"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "pending"
                    ],
                    "example": "pending"
                  },
                  "source_location_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 1
                  },
                  "destination_location_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 2
                  },
                  "reference_type": {
                    "type": "string",
                    "enum": [
                      "ticket",
                      "customer",
                      "schedule",
                      "manual"
                    ],
                    "nullable": true,
                    "example": "ticket"
                  },
                  "reference_id": {
                    "type": "integer",
                    "nullable": true,
                    "example": 123
                  },
                  "reason": {
                    "type": "string",
                    "nullable": true,
                    "example": "pasang"
                  },
                  "notes": {
                    "type": "string",
                    "nullable": true,
                    "example": "Material PSB pelanggan"
                  },
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": [
                        "item_id",
                        "quantity"
                      ],
                      "properties": {
                        "item_id": {
                          "type": "integer",
                          "example": 12
                        },
                        "quantity": {
                          "type": "number",
                          "example": 2
                        },
                        "notes": {
                          "type": "string",
                          "nullable": true,
                          "example": "ONU + adaptor"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Request diperbarui"
          },
          "422": {
            "description": "Status tidak bisa diedit"
          }
        }
      }
    },
    "/inventory/requests/{id}/approve": {
      "post": {
        "summary": "approve Request Barang",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status request diperbarui"
          },
          "422": {
            "description": "Transisi status tidak valid"
          }
        }
      }
    },
    "/inventory/requests/{id}/resume": {
      "post": {
        "summary": "resume Request Barang",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status request diperbarui"
          },
          "422": {
            "description": "Transisi status tidak valid"
          }
        }
      }
    },
    "/inventory/requests/{id}/process": {
      "post": {
        "summary": "process Request Barang",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status request diperbarui"
          },
          "422": {
            "description": "Transisi status tidak valid"
          }
        }
      }
    },
    "/inventory/requests/{id}/complete": {
      "post": {
        "summary": "complete Request Barang",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status request diperbarui"
          },
          "422": {
            "description": "Transisi status tidak valid"
          }
        }
      }
    },
    "/inventory/requests/{id}/reject": {
      "post": {
        "summary": "reject Request Barang",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "example": "Stok belum tersedia / alasan keputusan"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status request diperbarui"
          },
          "422": {
            "description": "Alasan wajib atau transisi status tidak valid"
          }
        }
      }
    },
    "/inventory/requests/{id}/defer": {
      "post": {
        "summary": "defer Request Barang",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "example": "Stok belum tersedia / alasan keputusan"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status request diperbarui"
          },
          "422": {
            "description": "Alasan wajib atau transisi status tidak valid"
          }
        }
      }
    },
    "/inventory/requests/{id}/cancel": {
      "post": {
        "summary": "cancel Request Barang",
        "tags": [
          "Inventory"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "example": "Stok belum tersedia / alasan keputusan"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status request diperbarui"
          },
          "422": {
            "description": "Alasan wajib atau transisi status tidak valid"
          }
        }
      }
    },
    "/attendance/mobile/status": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Status Absensi mobile hari ini",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Status hari ini, roster, setting, dan pending request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "description": "Mengembalikan status absensi hari ini, roster, setting bukti, status device, KPI bulan berjalan, request terbaru, server_time, dan skor risiko untuk portal /absensi."
      }
    },
    "/attendance/mobile/history": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Riwayat Absensi mobile pribadi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Riwayat absensi pribadi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/device": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Registrasi device Absensi mobile",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "device_uid"
                ],
                "properties": {
                  "device_uid": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "fingerprint_hash": {
                    "type": "string",
                    "maxLength": 128
                  },
                  "label": {
                    "type": "string"
                  },
                  "platform": {
                    "type": "string"
                  },
                  "meta": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Device terdaftar atau diperbarui",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Tabel attendance_devices belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/clock-in": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Check-in mobile kamera langsung",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "latitude": {
                    "type": "number"
                  },
                  "longitude": {
                    "type": "number"
                  },
                  "accuracy": {
                    "type": "number"
                  },
                  "selfie": {
                    "type": "string",
                    "description": "Data URL base64 hasil kamera langsung"
                  },
                  "selfie_source": {
                    "type": "string",
                    "enum": [
                      "camera"
                    ]
                  },
                  "device_uid": {
                    "type": "string"
                  },
                  "fingerprint_hash": {
                    "type": "string"
                  },
                  "platform": {
                    "type": "string"
                  },
                  "mock_location": {
                    "type": "boolean"
                  },
                  "camera_meta": {
                    "type": "object"
                  },
                  "user_agent": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  },
                  "liveness": {
                    "type": "object",
                    "properties": {
                      "prompt": {
                        "type": "string"
                      },
                      "completed": {
                        "type": "boolean"
                      },
                      "score": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 100
                      }
                    }
                  },
                  "site_code": {
                    "type": "string"
                  },
                  "offline_id": {
                    "type": "string"
                  },
                  "captured_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Absensi berhasil dicatat",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi GPS, kamera, device, radius, atau fraud gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "description": "Check-in/out mobile dengan kamera langsung. Setelah kamera aktif di /absensi, liveness berjalan otomatis; payload mengirim liveness.completed, score, prompt, dan metadata kamera."
      }
    },
    "/attendance/mobile/clock-out": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Check-out mobile kamera langsung",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "latitude": {
                    "type": "number"
                  },
                  "longitude": {
                    "type": "number"
                  },
                  "accuracy": {
                    "type": "number"
                  },
                  "selfie": {
                    "type": "string",
                    "description": "Data URL base64 hasil kamera langsung"
                  },
                  "selfie_source": {
                    "type": "string",
                    "enum": [
                      "camera"
                    ]
                  },
                  "device_uid": {
                    "type": "string"
                  },
                  "fingerprint_hash": {
                    "type": "string"
                  },
                  "platform": {
                    "type": "string"
                  },
                  "mock_location": {
                    "type": "boolean"
                  },
                  "camera_meta": {
                    "type": "object"
                  },
                  "user_agent": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  },
                  "liveness": {
                    "type": "object",
                    "properties": {
                      "prompt": {
                        "type": "string"
                      },
                      "completed": {
                        "type": "boolean"
                      },
                      "score": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 100
                      }
                    }
                  },
                  "site_code": {
                    "type": "string"
                  },
                  "offline_id": {
                    "type": "string"
                  },
                  "captured_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Absensi berhasil dicatat",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi GPS, kamera, device, radius, atau fraud gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "description": "Check-in/out mobile dengan kamera langsung. Setelah kamera aktif di /absensi, liveness berjalan otomatis; payload mengirim liveness.completed, score, prompt, dan metadata kamera."
      }
    },
    "/attendance/mobile/requests": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Buat pengajuan Absensi dari mobile",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "request_type",
                  "reason"
                ],
                "properties": {
                  "request_type": {
                    "type": "string",
                    "enum": [
                      "leave",
                      "sick",
                      "permission",
                      "correction"
                    ]
                  },
                  "start_date": {
                    "type": "string",
                    "format": "date"
                  },
                  "end_date": {
                    "type": "string",
                    "format": "date"
                  },
                  "work_date": {
                    "type": "string",
                    "format": "date"
                  },
                  "requested_check_in_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "requested_check_out_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "mobile"
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "minLength": 5
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pengajuan dibuat",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi pengajuan gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/devices": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Daftar device Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar device Absensi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Migration full suite belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/shifts": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Daftar shift Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar shift Absensi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Migration full suite belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Simpan shift Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Simpan shift Absensi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/rosters": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Daftar roster Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar roster Absensi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Migration full suite belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Simpan roster Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Simpan roster Absensi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/approval-flows": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Daftar approval flow Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar approval flow Absensi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Migration full suite belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Simpan approval flow Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Simpan approval flow Absensi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/payroll-detail": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Payroll detail Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Payroll detail Absensi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Migration full suite belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/devices/{deviceId}/status": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Update status trusted/blocked device Absensi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "trusted",
                      "blocked"
                    ]
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status device diperbarui",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/calendar": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Kalender shift mobile",
        "description": "Mengembalikan hari periode, roster, shift, dan status absensi user.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Kalender shift mobile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/approvals": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Approval Absensi mobile",
        "description": "Mengembalikan pengajuan dan lembur pending untuk admin/koordinator.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Approval Absensi mobile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/payroll": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Payroll pribadi mobile",
        "description": "Mengembalikan estimasi payroll/THP user untuk periode.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Payroll pribadi mobile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/site-code/verify": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Verifikasi QR/NFC lokasi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "site_code"
                ],
                "properties": {
                  "site_code": {
                    "type": "string",
                    "description": "Format HB:1, SCHEDULE:10, TICKET:25, atau SITE:kode"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verifikasi QR/NFC lokasi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "Verifikasi QR/NFC lokasi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/site-work/start": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Mulai visit/site work mobile",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "site_code": {
                    "type": "string",
                    "description": "Format HB:1, SCHEDULE:10, TICKET:25, atau SITE:kode"
                  },
                  "schedule_id": {
                    "type": "integer"
                  },
                  "ticket_id": {
                    "type": "integer"
                  },
                  "latitude": {
                    "type": "number"
                  },
                  "longitude": {
                    "type": "number"
                  },
                  "accuracy": {
                    "type": "number"
                  },
                  "photo": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mulai visit/site work mobile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "Mulai visit/site work mobile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/site-work/finish": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Selesai visit/site work mobile",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "work_log_id": {
                    "type": "integer"
                  },
                  "latitude": {
                    "type": "number"
                  },
                  "longitude": {
                    "type": "number"
                  },
                  "accuracy": {
                    "type": "number"
                  },
                  "photo": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Selesai visit/site work mobile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "Selesai visit/site work mobile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/offline-batch": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Kirim ulang absensi offline",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "items"
                ],
                "properties": {
                  "items": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "object",
                      "properties": {
                        "mode": {
                          "type": "string",
                          "enum": [
                            "in",
                            "out"
                          ]
                        },
                        "payload": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kirim ulang absensi offline",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "Kirim ulang absensi offline",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/technician/tasks": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Tugas teknisi hari ini",
        "description": "Mengembalikan jadwal hari ini, tiket terbuka, dan ringkasan KPI: open_work (perlu dikerjakan), inprogress_work (sedang diproses), done_work (selesai hari ini), active_work_logs (visit GPS aktif).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Tugas teknisi hari ini",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/technician/tickets": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Riwayat tiket teknisi mobile",
        "description": "Mengembalikan KPI dan daftar riwayat tiket yang terkait dengan user login di /absensi. Scope data dibatasi ke tiket yang di-assign, di-claim, anggota team, atau jadwal teknisi terkait user.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "today",
                "7d",
                "30d",
                "month",
                "custom"
              ],
              "default": "month"
            },
            "description": "Periode KPI dan riwayat tiket."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "pending",
                "inprogress",
                "success",
                "sla_breached",
                "cancelled"
              ],
              "default": "all"
            },
            "description": "Filter status riwayat tiket."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Tanggal mulai saat period=custom."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Tanggal selesai saat period=custom."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 120
            },
            "description": "Pencarian nomor tiket, tipe, deskripsi, atau nama pelanggan."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 10,
              "maximum": 100,
              "default": 50
            },
            "description": "Jumlah maksimal tiket yang dikembalikan."
          }
        ],
        "responses": {
          "200": {
            "description": "KPI dan riwayat tiket teknisi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Filter tidak valid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/tickets/{ticketId}/progress": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Tambah progress tiket dari portal /absensi.",
        "description": "Menyimpan catatan progress tiket dari teknisi. Jika tiket belum Diproses, sistem otomatis memindahkan status ke Diproses terlebih dahulu.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "ID tiket yang masuk daftar tugas teknisi login."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "notes"
                ],
                "properties": {
                  "latitude": {
                    "type": "number",
                    "nullable": true,
                    "minimum": -90,
                    "maximum": 90,
                    "example": -8.65
                  },
                  "longitude": {
                    "type": "number",
                    "nullable": true,
                    "minimum": -180,
                    "maximum": 180,
                    "example": 115.21
                  },
                  "accuracy": {
                    "type": "number",
                    "nullable": true,
                    "minimum": 0,
                    "example": 18
                  },
                  "notes": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 3000,
                    "example": "Cek ODP, redaman tinggi. Sedang ganti patchcord."
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "internal",
                      "public"
                    ],
                    "example": "internal"
                  },
                  "photo": {
                    "type": "string",
                    "nullable": true,
                    "description": "Satu foto progress base64 (legacy). Gunakan photos[] untuk multi-foto."
                  },
                  "photos": {
                    "type": "array",
                    "maxItems": 30,
                    "items": {
                      "type": "string"
                    },
                    "description": "Banyak foto progress base64 dari /absensi (maks 30 per kiriman)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Progress tiket tersimpan berhasil"
          },
          "422": {
            "description": "Validasi gagal dengan pesan Indonesia yang jelas."
          },
          "403": {
            "description": "User tidak berhak mengakses tiket ini."
          }
        }
      }
    },
    "/attendance/mobile/tickets/{ticketId}/reschedule": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Jadwalkan ulang tiket dari /absensi",
        "description": "Mengubah waktu eksekusi tiket aktif dan jadwal teknisi terkait jika tiket tersebut termasuk daftar tugas teknisi login.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scheduled_execution_at",
                  "reason"
                ],
                "properties": {
                  "scheduled_execution_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "customer_not_home",
                      "team_unavailable",
                      "other"
                    ]
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 1000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tiket berhasil dijadwalkan ulang"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    },
    "/money-requests/pending-count": {
      "get": {
        "summary": "Jumlah Request Uang pending untuk badge NetAdmin",
        "tags": [
          "Finance"
        ],
        "responses": {
          "200": {
            "description": "Objek { count: number }"
          }
        }
      }
    },
    "/attendance/mobile/money-requests": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Buat Request Uang teknisi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "title",
                  "amount",
                  "description",
                  "receipt"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "operational",
                      "reimbursement",
                      "purchase",
                      "fuel",
                      "advance",
                      "other"
                    ]
                  },
                  "title": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "number"
                  },
                  "needed_date": {
                    "type": "string",
                    "format": "date"
                  },
                  "vendor": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "receipt": {
                    "type": "string",
                    "description": "Data URL JPG/PNG/WEBP/PDF maksimal 5 MB"
                  },
                  "schedule_id": {
                    "type": "integer"
                  },
                  "ticket_id": {
                    "type": "integer"
                  },
                  "site_code": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Buat Request Uang teknisi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "Buat Request Uang teknisi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/inventory/catalog": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Katalog Request Barang teknisi per Gudang Asal",
        "description": "Mengembalikan Gudang Asal sesuai Home Base teknisi, pilihan referensi tiket/jadwal user login, dan daftar item yang benar-benar memiliki stok tersedia pada source_location_id terpilih. Tanpa Gudang Asal terpilih, response meminta user memilih gudang lebih dulu.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Katalog barang untuk teknisi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "source_location_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "ID Gudang Asal/POP. Jika dikirim, daftar items hanya berisi stok tersedia dari gudang tersebut."
          }
        ]
      }
    },
    "/attendance/mobile/inventory/requests": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Buat Request Barang teknisi",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason",
                  "items"
                ],
                "properties": {
                  "reason": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  },
                  "destination_location_id": {
                    "type": "integer"
                  },
                  "schedule_id": {
                    "type": "integer"
                  },
                  "ticket_id": {
                    "type": "integer"
                  },
                  "site_code": {
                    "type": "string"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "item_id": {
                          "type": "integer"
                        },
                        "quantity": {
                          "type": "number"
                        },
                        "notes": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Buat Request Barang teknisi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "Buat Request Barang teknisi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "description": "Membuat Request Barang multi-item dari /absensi. Body memakai items[] dengan satu Gudang Asal dan satu Referensi. Backend memvalidasi Gudang Asal sesuai Home Base, item tidak duplikat, quantity tersedia di gudang tersebut, dan referensi tiket/jadwal memang terkait user login."
      }
    },
    "/attendance/mobile/tracking/status": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Status GPS Tracking /absensi",
        "description": "Mengembalikan setting tracking, visit aktif, titik terakhir, dan status stale untuk user login.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Status GPS Tracking",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/tracking/point": {
      "post": {
        "tags": [
          "Attendance"
        ],
        "summary": "Simpan titik GPS Tracking",
        "description": "Menyimpan titik GPS Tracking untuk visit/site work aktif. Digunakan oleh /absensi selama halaman aktif.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "latitude",
                  "longitude"
                ],
                "properties": {
                  "work_log_id": {
                    "type": "integer"
                  },
                  "latitude": {
                    "type": "number",
                    "minimum": -90,
                    "maximum": 90
                  },
                  "longitude": {
                    "type": "number",
                    "minimum": -180,
                    "maximum": 180
                  },
                  "accuracy": {
                    "type": "number"
                  },
                  "speed": {
                    "type": "number"
                  },
                  "heading": {
                    "type": "number"
                  },
                  "captured_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "device_uid": {
                    "type": "string"
                  },
                  "offline_id": {
                    "type": "string"
                  },
                  "platform": {
                    "type": "string"
                  },
                  "user_agent": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Titik tersimpan",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/tracking/history": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Riwayat GPS Tracking",
        "description": "Mengambil riwayat titik GPS Tracking milik user login, opsional filter work_log_id dan limit.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "work_log_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 20,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Riwayat tracking",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "422": {
            "description": "Validasi gagal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema belum tersedia",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/attendance/mobile/incentives": {
      "get": {
        "tags": [
          "Attendance"
        ],
        "summary": "Insentif teknisi mobile",
        "description": "Mengembalikan estimasi bonus, status review, hold pencairan, payout, dan syarat yang tampil di /absensi.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Ringkasan insentif teknisi",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Schema Absensi belum tersedia"
          }
        }
      }
    },
    "/attendance/incentives/settings": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Pengaturan Insentif Teknisi",
        "description": "Baca aturan bonus, punishment, syarat, deskripsi, hold, dan status modul Insentif Teknisi tenant.",
        "responses": {
          "200": {
            "description": "Pengaturan insentif"
          }
        }
      },
      "put": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Simpan Pengaturan Insentif Teknisi",
        "description": "Simpan aturan bonus/punishment configurable per tenant. Semua nominal, satuan, hold, syarat, dan deskripsi divalidasi dengan pesan jelas.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pengaturan disimpan"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/attendance/incentives/review": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Review Insentif Teknisi",
        "description": "Baca draft insentif dari tiket/jadwal/work log serta status review dan payout.",
        "responses": {
          "200": {
            "description": "Daftar review insentif"
          }
        }
      },
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Simpan Review Insentif",
        "description": "Set status pending/approved/rejected, nominal disetujui, quantity, dan catatan review.",
        "responses": {
          "200": {
            "description": "Review disimpan"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/attendance/incentives/payout": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Payout Insentif Teknisi",
        "description": "Tandai item bonus approved sebagai paid dan catat referensi payout.",
        "responses": {
          "200": {
            "description": "Payout disimpan"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/attendance/mobile/tickets/{ticketId}/process": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Proses tiket dari portal /absensi.",
        "description": "Mengubah tiket menjadi Diproses dari /absensi mobile, klaim tiket untuk teknisi login bila valid, dan menyimpan GPS process bila tersedia.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "ID tiket yang masuk daftar tugas teknisi login."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "latitude": {
                    "type": "number",
                    "nullable": true,
                    "minimum": -90,
                    "maximum": 90,
                    "example": -8.65
                  },
                  "longitude": {
                    "type": "number",
                    "nullable": true,
                    "minimum": -180,
                    "maximum": 180,
                    "example": 115.21
                  },
                  "accuracy": {
                    "type": "number",
                    "nullable": true,
                    "minimum": 0,
                    "example": 18
                  },
                  "notes": {
                    "type": "string",
                    "maxLength": 2000,
                    "example": "Mulai pengecekan redaman di lokasi pelanggan."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tiket diproses berhasil"
          },
          "422": {
            "description": "Validasi gagal dengan pesan Indonesia yang jelas."
          },
          "403": {
            "description": "User tidak berhak mengakses tiket ini."
          }
        }
      }
    },
    "/attendance/mobile/tickets/{ticketId}/psb-checklist": {
      "get": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Ambil draft checklist PSB mobile.",
        "description": "Mengembalikan status checklist PSB untuk teknisi login: foto wajib, material terpakai, pilihan ONT/SN dari Request Barang terkait, daftar kekurangan, dan flag apakah tiket boleh diselesaikan.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Checklist PSB berhasil dibaca"
          },
          "422": {
            "description": "Tiket bukan PSB atau jadwal teknisi belum terhubung."
          },
          "403": {
            "description": "User tidak berhak mengakses tiket ini."
          }
        }
      }
    },
    "/attendance/mobile/tickets/{ticketId}/psb-checklist/photos": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Autosave foto checklist PSB.",
        "description": "Menyimpan foto PSB yang dikirim sebagai data URL JPG/PNG/WEBP dari /absensi. Browser mobile melakukan auto-resize sebelum upload; backend tetap memvalidasi format dan ukuran maksimal 5 MB.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "category",
                  "photo"
                ],
                "properties": {
                  "category": {
                    "type": "string",
                    "enum": [
                      "psb_odp_photo",
                      "psb_odp_power_photo",
                      "psb_odp_label_port_photo",
                      "psb_house_front_photo",
                      "psb_ont_photo",
                      "psb_ont_power_photo",
                      "psb_ont_installed_photo",
                      "psb_speedtest_photo"
                    ]
                  },
                  "photo": {
                    "type": "string",
                    "example": "data:image/jpeg;base64,..."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Foto tersimpan dan status checklist terbaru dikembalikan"
          },
          "422": {
            "description": "Kategori/foto tidak valid atau tiket tidak bisa dikoreksi."
          }
        }
      }
    },
    "/attendance/mobile/tickets/{ticketId}/psb-checklist/materials": {
      "put": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Simpan material checklist PSB.",
        "description": "Menyimpan ONT/SN dari Request Barang tiket, meter kabel terpakai, jumlah konektor SC, pigtail, serta item tambahan opsional. Sistem mengembalikan warning jika pemakaian melebihi request barang.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cable_length_m",
                  "sc_connector_qty",
                  "pigtail_qty"
                ],
                "properties": {
                  "onu_item_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "onu_serial_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "onu_serial": {
                    "type": "string",
                    "nullable": true
                  },
                  "cable_length_m": {
                    "type": "number",
                    "minimum": 0
                  },
                  "sc_connector_qty": {
                    "type": "number",
                    "minimum": 0
                  },
                  "pigtail_qty": {
                    "type": "number",
                    "minimum": 0
                  },
                  "optional_items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "quantity": {
                          "type": "number"
                        },
                        "unit": {
                          "type": "string"
                        },
                        "notes": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Material tersimpan dan status checklist terbaru dikembalikan"
          },
          "422": {
            "description": "ONT/material tidak valid atau tiket tidak bisa dikoreksi."
          }
        }
      }
    },
    "/attendance/mobile/tickets/{ticketId}/resolve": {
      "post": {
        "tags": [
          "Netadmin Attendance"
        ],
        "summary": "Selesaikan tiket dari portal /absensi.",
        "description": "Menyelesaikan tiket dari HP teknisi dengan catatan resolusi minimal 15 karakter dan GPS resolve bila tersedia. Sistem menolak catatan yang terlalu pendek agar tiket tidak ditutup tanpa dokumentasi.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "ID tiket yang masuk daftar tugas teknisi login."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "resolution"
                ],
                "properties": {
                  "latitude": {
                    "type": "number",
                    "nullable": true,
                    "minimum": -90,
                    "maximum": 90,
                    "example": -8.65
                  },
                  "longitude": {
                    "type": "number",
                    "nullable": true,
                    "minimum": -180,
                    "maximum": 180,
                    "example": 115.21
                  },
                  "accuracy": {
                    "type": "number",
                    "nullable": true,
                    "minimum": 0,
                    "example": 18
                  },
                  "resolution": {
                    "type": "string",
                    "minLength": 15,
                    "maxLength": 5000,
                    "example": "Kabel drop putus sudah diganti, redaman normal, pelanggan sudah online."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tiket diselesaikan berhasil"
          },
          "422": {
            "description": "Validasi gagal dengan pesan Indonesia yang jelas."
          },
          "403": {
            "description": "User tidak berhak mengakses tiket ini."
          }
        }
      }
    },
    "/settings": {
      "get": {
        "summary": "Baca pengaturan tenant",
        "description": "Endpoint internal NetAdmin untuk membaca pengaturan tenant. Response menyertakan ai_assistant_enabled, ai_provider (anthropic|openai|gemini|deepseek|aksita), ai_models, dan ai_key_sets untuk visibilitas tombol Bantuan AI per tenant.",
        "tags": [
          "Internal Settings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pengaturan tenant",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ai_assistant_enabled": {
                      "type": "boolean",
                      "default": false,
                      "description": "Jika true, tombol robot dan widget Bantuan AI ditampilkan."
                    },
                    "ai_provider": {
                      "type": "string",
                      "enum": ["anthropic", "openai", "gemini", "deepseek", "aksita"],
                      "description": "Provider AI aktif untuk AI Assistant."
                    },
                    "ai_providers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "value": { "type": "string" },
                          "label": { "type": "string" }
                        }
                      }
                    },
                    "ops_settings": {
                      "type": "object",
                      "properties": {
                        "ai_assistant_enabled": {
                          "type": "boolean",
                          "default": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "put": {
        "summary": "Simpan pengaturan tenant",
        "description": "Endpoint internal NetAdmin. Gunakan ai_provider, ai_model, ai_api_key, dan ops_settings.ai_assistant_enabled untuk konfigurasi AI Assistant. Provider deepseek menyimpan key di ops_settings.ai_deepseek_api_key dan memakai https://api.deepseek.com/chat/completions.",
        "tags": [
          "Internal Settings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ai_provider": {
                    "type": "string",
                    "enum": ["anthropic", "openai", "gemini", "deepseek", "aksita"]
                  },
                  "ai_model": {
                    "type": "string",
                    "example": "deepseek-v4-flash"
                  },
                  "ai_api_key": {
                    "type": "string",
                    "description": "API key provider yang sedang dipilih. Disimpan per-provider (mis. ai_deepseek_api_key untuk deepseek)."
                  },
                  "ops_settings": {
                    "type": "object",
                    "properties": {
                      "ai_assistant_enabled": {
                        "type": "boolean",
                        "default": false
                      }
                    }
                  }
                }
              },
              "example": {
                "ops_settings": {
                  "ai_assistant_enabled": true
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pengaturan tersimpan"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/tickets/incidents": {
      "get": {
        "summary": "Daftar incident aktif",
        "description": "Endpoint internal NetAdmin untuk menampilkan incident massal aktif beserta jumlah tiket anak dan metadata incident.",
        "tags": [
          "Internal Tickets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar incident aktif"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "summary": "Buat incident massal",
        "description": "Membuat incident tiket massal. Jika auto_link_tickets=true, backend menautkan tiket Open/In Progress yang cocok dengan OLT dan/atau Home Base. Broadcast WA ke pelanggan hanya berjalan bila notify_customers=true.",
        "tags": [
          "Internal Tickets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "description"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 100,
                    "example": "Incident / Gangguan Massal"
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "high",
                      "medium",
                      "low"
                    ],
                    "default": "high"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 5000,
                    "example": "OLT-CORE-01 down, pelanggan area Batam Center terdampak"
                  },
                  "olt_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "home_base_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "severity": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "critical",
                      "major",
                      "minor"
                    ],
                    "example": "major"
                  },
                  "estimated_recovery_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  },
                  "impact_area": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 500
                  },
                  "root_cause_initial": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 1000
                  },
                  "impact_ticket_description": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 2000,
                    "description": "Template deskripsi tiket anak incident. Mendukung placeholder {incident_number}, {incident_description}, {impact_area}, {eta}, {customer_name}, {customer_phone}, {customer_address}, {onu_name}, dan {onu_sn}."
                  },
                  "group_message": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 3000,
                    "description": "Pesan WhatsApp group operasional. Jika kosong, backend memakai ringkasan incident bawaan."
                  },
                  "auto_link_tickets": {
                    "type": "boolean",
                    "default": false
                  },
                  "create_impact_tickets": {
                    "type": "boolean",
                    "default": false,
                    "description": "Jika true, sistem membuat tiket anak untuk pelanggan dengan ONU pada OLT/Home Base terdampak dan menghindari duplikat tiket aktif."
                  },
                  "schedule_impact_tickets": {
                    "type": "boolean",
                    "default": true,
                    "description": "Jika true bersama create_impact_tickets, sistem membuat jadwal teknisi pending untuk tiap tiket anak incident."
                  },
                  "notify_customers": {
                    "type": "boolean",
                    "default": false
                  },
                  "notify_whatsapp_groups": {
                    "type": "boolean",
                    "default": false,
                    "description": "Jika true, sistem mengirim ringkasan incident ke group WhatsApp yang aktif untuk event ticket_incident_created / Incident Massal."
                  },
                  "auto_link_limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 50
                  },
                  "impact_ticket_limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 50
                  }
                }
              },
              "example": {
                "description": "OLT-CORE-01 down, pelanggan area Batam Center terdampak",
                "olt_id": 12,
                "home_base_id": 3,
                "severity": "major",
                "estimated_recovery_at": "2026-06-29T22:00:00+08:00",
                "impact_area": "Batam Center Cluster A-B",
                "root_cause_initial": "Indikasi uplink putus",
                "impact_ticket_description": "Tiket otomatis dari incident {incident_number}.\n\nRingkasan gangguan:\n{incident_description}\n\nPelanggan terdampak: {customer_name}\nONU/SN: {onu_sn}\nArea terdampak: {impact_area}\nEstimasi pulih: {eta}",
                "group_message": "*INCIDENT MASSAL*\n\nArea terdampak: Batam Center Cluster A-B\nSeverity: Major\nMohon NOC dan koordinator teknisi memantau tiket incident sampai normal.",
                "auto_link_tickets": true,
                "create_impact_tickets": true,
                "schedule_impact_tickets": true,
                "notify_customers": true,
                "notify_whatsapp_groups": true,
                "auto_link_limit": 50,
                "impact_ticket_limit": 50
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Incident dibuat dengan incident_summary"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/tickets/{id}/assignment-candidates": {
      "get": {
        "summary": "Rekomendasi teknisi assign manual",
        "description": "Endpoint internal NetAdmin untuk menampilkan teknisi available dan home base terdekat saat assign manual tiket.",
        "tags": [
          "Internal Tickets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar kandidat teknisi"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/tickets/{id}/manual-assign": {
      "post": {
        "summary": "Assign manual tiket",
        "description": "Assign manual menolak teknisi sibuk atau bukan home base terdekat kecuali allow_override=true dan alasan diisi jelas.",
        "tags": [
          "Internal Tickets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "assigned_to",
                  "reason"
                ],
                "properties": {
                  "assigned_to": {
                    "type": "integer"
                  },
                  "reason": {
                    "type": "string",
                    "minLength": 5,
                    "maxLength": 500
                  },
                  "clear_claim": {
                    "type": "boolean",
                    "default": true
                  },
                  "allow_override": {
                    "type": "boolean",
                    "default": false
                  }
                }
              },
              "example": {
                "assigned_to": 12,
                "reason": "Teknisi rekomendasi tidak tersedia, override disetujui koordinator.",
                "clear_claim": true,
                "allow_override": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tiket berhasil diassign"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/tickets/{id}/link-incident": {
      "post": {
        "summary": "Tautkan tiket ke incident",
        "description": "Endpoint internal NetAdmin untuk menautkan tiket biasa ke incident aktif. Manual link tetap mengirim notifikasi pelanggan sesuai pengaturan tiket tenant.",
        "tags": [
          "Internal Tickets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "parent_ticket_id"
                ],
                "properties": {
                  "parent_ticket_id": {
                    "type": "integer",
                    "description": "ID ticket incident tujuan"
                  }
                }
              },
              "example": {
                "parent_ticket_id": 9001
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tiket ditautkan ke incident"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/tickets/{id}/unlink-incident": {
      "post": {
        "summary": "Lepas tiket dari incident",
        "description": "Endpoint internal NetAdmin untuk melepas tiket biasa dari incident tanpa menghapus histori aktivitas.",
        "tags": [
          "Internal Tickets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tiket dilepas dari incident"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/customers/import/sisbro/preview": {
      "post": {
        "summary": "Preview import pelanggan Sisbro",
        "description": "Memvalidasi CSV Sisbro tanpa menulis data. Mengabaikan baris kosong, memetakan No Pelanggan sebagai kode pelanggan, dan mengembalikan error/warning per baris dengan solusi.",
        "tags": [
          "Internal Customers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "csv"
                ],
                "properties": {
                  "csv": {
                    "type": "string",
                    "description": "Isi CSV Sisbro delimiter titik koma (;). Header wajib: No Pelanggan, Nama, Telepon, Paket, Status."
                  },
                  "import_scope": {
                    "type": "string",
                    "enum": [
                      "full",
                      "internet",
                      "customer",
                      "billing"
                    ],
                    "default": "full"
                  },
                  "nas_key": {
                    "type": "string",
                    "nullable": true,
                    "description": "Wajib hanya jika provision_network=true."
                  },
                  "create_missing_packages": {
                    "type": "boolean",
                    "default": true
                  },
                  "provision_network": {
                    "type": "boolean",
                    "default": false
                  },
                  "migration_billing": {
                    "type": "string",
                    "enum": [
                      "skip",
                      "create_paid",
                      "paid_until"
                    ],
                    "default": "skip"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import Sisbro valid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "can_import": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "object"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "preview_rows": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "CSV Sisbro tidak valid, paket/kolom/data duplikat, atau opsi import tidak lengkap",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "can_import": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "object"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "preview_rows": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "User tidak memiliki permission customers.create"
          }
        }
      }
    },
    "/customers/import/sisbro/commit": {
      "post": {
        "summary": "Commit import pelanggan Sisbro",
        "description": "Mengimport atau mengupdate pelanggan dari CSV Sisbro setelah preview bersih. Default aman untuk migrasi: migration_billing=skip dan provision_network=false.",
        "tags": [
          "Internal Customers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "csv"
                ],
                "properties": {
                  "csv": {
                    "type": "string",
                    "description": "Isi CSV Sisbro delimiter titik koma (;). Header wajib: No Pelanggan, Nama, Telepon, Paket, Status."
                  },
                  "import_scope": {
                    "type": "string",
                    "enum": [
                      "full",
                      "internet",
                      "customer",
                      "billing"
                    ],
                    "default": "full"
                  },
                  "nas_key": {
                    "type": "string",
                    "nullable": true,
                    "description": "Wajib hanya jika provision_network=true."
                  },
                  "create_missing_packages": {
                    "type": "boolean",
                    "default": true
                  },
                  "provision_network": {
                    "type": "boolean",
                    "default": false
                  },
                  "migration_billing": {
                    "type": "string",
                    "enum": [
                      "skip",
                      "create_paid",
                      "paid_until"
                    ],
                    "default": "skip"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import Sisbro selesai",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "can_import": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "object"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "preview_rows": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "CSV Sisbro tidak valid, paket/kolom/data duplikat, atau opsi import tidak lengkap",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "can_import": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "object"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "preview_rows": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "User tidak memiliki permission customers.create"
          }
        }
      }
    },
    "/customers/{id}/email-account": {
      "post": {
        "summary": "Generate email pelanggan existing",
        "description": "Membuat mailbox Virtualmin untuk pelanggan existing, menyimpan email ke data pelanggan, dan mengembalikan password awal jika mailbox baru dibuat. Error dibuat jelas untuk permission, domain, format email, koneksi, login, atau kegagalan Virtualmin.",
        "tags": [
          "Internal Customers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "nullable": true,
                    "description": "Opsional. Jika kosong, local-part dibuat dari nama pelanggan."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Email berhasil dibuat/disimpan",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "email": {
                      "type": "string"
                    },
                    "password": {
                      "type": "string",
                      "nullable": true
                    },
                    "exists": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "User tidak memiliki permission customers.update"
          },
          "404": {
            "description": "Pelanggan tidak ditemukan"
          },
          "422": {
            "description": "Email/domain/konfigurasi Virtualmin tidak valid"
          },
          "502": {
            "description": "Virtualmin gagal dihubungi atau gagal membuat mailbox"
          }
        }
      }
    },
    "/platform/tenants/{tenant}/provisioning/subscriber-domain/regenerate-preview": {
      "post": {
        "summary": "Regenerate preview domain subscriber",
        "description": "Membuat kandidat domain subscriber otomatis dari slug tenant, melewati domain aktif dan domain yang sudah dipakai, mengecek job provisioning berjalan, konflik domain, dan readiness DNS. Endpoint ini hanya preview, tidak membuat DNS dan tidak mengubah domain tenant.",
        "tags": [
          "Internal Tenant Provisioning"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tenant",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "nullable": true,
                    "description": "Domain dasar subscriber. Default TENANT_SUBSCRIBER_BASE_DOMAIN / kliniktech.com."
                  },
                  "dns_target": {
                    "type": "string",
                    "nullable": true,
                    "description": "Target DNS A/CNAME. Jika kosong memakai konfigurasi tenant subscriber default."
                  },
                  "record_type": {
                    "type": "string",
                    "enum": [
                      "A",
                      "CNAME"
                    ],
                    "default": "A"
                  },
                  "proxied": {
                    "type": "boolean",
                    "default": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kandidat domain subscriber siap dipreview"
          },
          "409": {
            "description": "Tenant masih memiliki provisioning job berjalan"
          },
          "422": {
            "description": "Tenant master ditolak, domain dasar invalid, konflik domain, atau kandidat domain habis"
          },
          "403": {
            "description": "User tidak memiliki akses Platform Admin"
          }
        }
      }
    },
    "/auth/sessions": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Lihat sesi login NetAdmin akun aktif",
        "description": "Mengembalikan pengaturan Login Multi Device dan daftar sesi/device NetAdmin milik user yang sedang login. Membutuhkan permission account.security.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Setting dan daftar sesi berhasil dimuat"
          },
          "403": {
            "description": "User tidak memiliki permission account.security"
          }
        }
      }
    },
    "/auth/sessions/settings": {
      "put": {
        "tags": [
          "Auth"
        ],
        "summary": "Simpan pengaturan Login Multi Device",
        "description": "Menyimpan toggle multi_device_enabled dan batas multi_device_limit. Saat multi-device nonaktif, sesi/device lain otomatis dicabut.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "multi_device_enabled",
                  "multi_device_limit"
                ],
                "properties": {
                  "multi_device_enabled": {
                    "type": "boolean",
                    "example": true
                  },
                  "multi_device_limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "example": 3
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pengaturan berhasil disimpan"
          },
          "403": {
            "description": "User tidak memiliki permission account.security"
          },
          "422": {
            "description": "Input toggle atau batas device tidak valid"
          },
          "503": {
            "description": "Migration kolom multi-device users belum dijalankan"
          }
        }
      }
    },
    "/auth/sessions/{tokenId}": {
      "delete": {
        "tags": [
          "Auth"
        ],
        "summary": "Cabut sesi/device NetAdmin tertentu",
        "description": "Mencabut sesi NetAdmin milik akun aktif berdasarkan tokenId. Device yang sedang dipakai tidak dapat dicabut lewat endpoint ini.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tokenId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sesi/device berhasil dicabut"
          },
          "403": {
            "description": "User tidak memiliki permission account.security"
          },
          "404": {
            "description": "Sesi/device tidak ditemukan atau sudah dicabut"
          },
          "422": {
            "description": "Device yang sedang dipakai tidak bisa dicabut dari daftar ini"
          }
        }
      }
    },
    "/auth/sessions/revoke-others": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Cabut semua sesi/device NetAdmin lain",
        "description": "Mencabut seluruh sesi NetAdmin lain milik akun aktif dan mempertahankan sesi yang sedang dipakai.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sesi/device lain berhasil dicabut atau tidak ada sesi lain"
          },
          "403": {
            "description": "User tidak memiliki permission account.security"
          }
        }
      }
    },
    "/office-assets": {
      "get": {
        "tags": [
          "Office Assets"
        ],
        "summary": "Daftar Aset Kantor",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "borrowed",
                "maintenance",
                "damaged",
                "lost",
                "sold",
                "disposed"
              ]
            }
          },
          {
            "name": "condition",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "good",
                "fair",
                "needs_repair",
                "broken",
                "lost"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar aset kantor"
          },
          "503": {
            "description": "Tabel office_assets belum dimigrasikan"
          }
        }
      },
      "post": {
        "tags": [
          "Office Assets"
        ],
        "summary": "Tambah Aset Kantor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "asset_code": {
                    "type": "string",
                    "example": "AST-2606-0001"
                  },
                  "name": {
                    "type": "string",
                    "example": "Laptop Admin NOC"
                  },
                  "category": {
                    "type": "string",
                    "example": "Laptop"
                  },
                  "serial_number": {
                    "type": "string",
                    "nullable": true
                  },
                  "location": {
                    "type": "string",
                    "nullable": true
                  },
                  "pic_user_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "purchase_price": {
                    "type": "number",
                    "nullable": true
                  },
                  "warranty_until": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Aset kantor dibuat"
          },
          "422": {
            "description": "Validasi field gagal dengan pesan jelas"
          }
        }
      }
    },
    "/office-assets/{id}": {
      "get": {
        "tags": [
          "Office Assets"
        ],
        "summary": "Detail Aset Kantor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Detail aset dan riwayat"
          },
          "404": {
            "description": "Aset tidak ditemukan"
          }
        }
      },
      "put": {
        "tags": [
          "Office Assets"
        ],
        "summary": "Update Aset Kantor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Aset kantor diperbarui"
          },
          "422": {
            "description": "Validasi field gagal"
          }
        }
      }
    },
    "/office-assets/{id}/move": {
      "post": {
        "tags": [
          "Office Assets"
        ],
        "summary": "Mutasi lokasi/PIC Aset Kantor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "location"
                ],
                "properties": {
                  "location": {
                    "type": "string"
                  },
                  "pic_user_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "borrowed"
                    ]
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Mutasi aset dicatat"
          },
          "422": {
            "description": "Lokasi/PIC tidak valid"
          }
        }
      }
    },
    "/office-assets/{id}/maintenance": {
      "post": {
        "tags": [
          "Office Assets"
        ],
        "summary": "Catat maintenance Aset Kantor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "condition",
                  "notes"
                ],
                "properties": {
                  "condition": {
                    "type": "string",
                    "enum": [
                      "good",
                      "fair",
                      "needs_repair",
                      "broken",
                      "lost"
                    ]
                  },
                  "cost": {
                    "type": "number"
                  },
                  "next_maintenance_at": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Maintenance dicatat"
          },
          "422": {
            "description": "Catatan/kondisi tidak valid"
          }
        }
      }
    },
    "/office-assets/{id}/audit": {
      "post": {
        "tags": [
          "Office Assets"
        ],
        "summary": "Opname Aset Kantor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "condition",
                  "location"
                ],
                "properties": {
                  "condition": {
                    "type": "string"
                  },
                  "location": {
                    "type": "string"
                  },
                  "pic_user_id": {
                    "type": "integer",
                    "nullable": true
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Opname dicatat"
          },
          "422": {
            "description": "Kondisi/lokasi tidak valid"
          }
        }
      }
    },
    "/office-assets/{id}/dispose": {
      "post": {
        "tags": [
          "Office Assets"
        ],
        "summary": "Disposal, dijual, atau hilang",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status",
                  "notes"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "sold",
                      "disposed",
                      "lost"
                    ]
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status akhir dicatat"
          },
          "422": {
            "description": "Alasan wajib atau status tidak valid"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Office Assets",
      "description": "Aset Kantor untuk register, mutasi, maintenance, opname, dan disposal aset milik kantor."
    }
  ]
}
