├── .env.example ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FREEMIUM_COMPARISON.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── composer.json ├── config └── satusehatintegration.php ├── configure.php ├── database ├── migrations │ ├── create_kode_wilayah_indonesia_table.php.stub │ ├── create_satusehat_icd10_table.php.stub │ ├── create_satusehat_log_table.php.stub │ └── create_satusehat_token_table.php.stub └── seeders │ ├── Icd10Seeder.php.stub │ ├── KodeWilayahIndonesiaSeeder.php.stub │ └── csv │ ├── icd10.csv.stub │ └── kode_wilayah_indonesia.csv.stub ├── resync-tags.bat └── src ├── Exception ├── FHIR │ ├── FHIRException.php │ ├── FHIRInvalidPropertyValue.php │ └── FHIRMissingProperty.php ├── Helper │ └── OAuth2ClientException.php └── Terminology │ ├── TerminologyException.php │ ├── TerminologyInvalidArgumentException.php │ └── TerminologyMissingArgumentException.php ├── FHIR ├── Bundle.php ├── Condition.php ├── Encounter.php ├── Location.php ├── Observation.php ├── Organization.php ├── Patient.php └── Practitioner.php ├── KYC.php ├── Models ├── SatusehatLog.php └── SatusehatToken.php ├── OAuth2Client.php ├── SatusehatIntegrationServiceProvider.php └── Terminology ├── Icd10.php ├── Kfa.php └── KodeWilayahIndonesia.php /.env.example: -------------------------------------------------------------------------------- 1 | # DATABASE UMUM SETUP 2 | DB_CONNECTION=mysql 3 | DB_HOST=127.0.0.1 4 | DB_PORT=3306 5 | DB_DATABASE=laravel 6 | DB_USERNAME=root 7 | DB_PASSWORD= 8 | 9 | # DATABASE MASTERDATA 10 | DB_CONNECTION_MASTER=mysql 11 | 12 | # SETUP ALAMAT & INFORMASI PENDUKUNG FASKES 13 | ALAMAT='Jl. Google Amazon Azure Raya' 14 | KOTA='Jakarta' 15 | KODE_POS='12345' 16 | KODE_PROVINSI='31' 17 | KODE_KABUPATEN='3171' 18 | KODE_KECAMATAN='317101' 19 | KODE_KELURAHAN='31710101' 20 | 21 | WEBSITE='https://www.google.com/' 22 | PHONE='021-88888888' 23 | EMAIL='google@google.com' 24 | LATITUDE=-6.23115426275766 25 | LONGITUDE=106.8431396484375 26 | 27 | # SATUSEHAT ENV 28 | SATUSEHAT_ENV=DEV 29 | 30 | SATUSEHAT_AUTH_DEV=https://api-satusehat-dev.dto.kemkes.go.id/oauth2/v1 31 | SATUSEHAT_FHIR_DEV=https://api-satusehat-dev.dto.kemkes.go.id/fhir-r4/v1 32 | 33 | SATUSEHAT_AUTH_STG=https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1 34 | SATUSEHAT_FHIR_STG=https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1 35 | 36 | SATUSEHAT_AUTH_PROD=https://api-satusehat.kemkes.go.id/oauth2/v1 37 | SATUSEHAT_FHIR_PROD=https://api-satusehat.kemkes.go.id/fhir-r4/v1 38 | 39 | ORGID_DEV=xxxxxx 40 | CLIENTID_DEV=xxxxxx 41 | CLIENTSECRET_DEV=xxxxxx 42 | 43 | ORGID_STG=xxxxxx 44 | CLIENTID_STG=xxxxxx 45 | CLIENTSECRET_STG=xxxxxx 46 | 47 | ORGID_PROD=xxxxxx 48 | CLIENTID_PROD=xxxxxx 49 | CLIENTSECRET_PROD=xxxxxx 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.0-stable - 2025-02-25 4 | 5 | ### What's Changed 6 | 7 | * Bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot in https://github.com/ivanwilliammd/satusehat-integration/pull/141 8 | * Updated support for Laravel 12 9 | 10 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/3.0.0-stable...3.1.0-stable 11 | 12 | ## 3.0.0-stable - 2025-01-30 13 | 14 | FULLY Tested functionality for Fase 1 Rawat Jalan SATUSEHAT 15 | 16 | ### Kualitas Kode dan Dukungan 17 | 18 | | | Free | 19 | |----------|------| 20 | | Functional Testing | ⚠️ | 21 | | Code Quality Check | ❌ | 22 | | Dukungan integrasi | ❌ | 23 | | Tanya Jawab | ❌ | 24 | | Pelatihan FHIR | ❌ | 25 | | Pelatihan Terminologi | ❌ | 26 | 27 | ### Integrasi Terminologi 28 | 29 | | | Terminologi | Free | 30 | |---|-------------|------| 31 | |1 | ICD-10: Diagnosis | ✅ | 32 | |2 | ICD-9CM: Procedure | ❌ | 33 | |3 | Kode Wilayah Indonesia | ✅ | 34 | |4 | KFA API (v2) | ✅ | 35 | |5 | LOINC and LOINC Answer: for Lab & Radiology | ❌ | 36 | |6 | SNOMED-CT | ❌ | 37 | |7 | KPTL (Kode Pembiayaan Tindakan dan Layanan Kesehatan) | ❌ | 38 | |8 | Units of Measure | ❌ | 39 | |9 | CVX (CDC Vaccine Codes) | ❌ | 40 | |10 | Terminology Kemkes (upd. Apr 2023) | ❌ | 41 | |11 | FHIR Value Set dan Terminology | ❌ | 42 | 43 | ### Integrasi dasar SATUSEHAT 44 | 45 | | | Fitur | Free | 46 | |---|-------------|------| 47 | | 1 | Basic - Otentikasi | ✅ | 48 | | 2 | Basic - KYC Centang Biru | ✅ | 49 | | 3 | Data - Batch/Bundle Kunjungan Diagnosis | ✅ | 50 | | 4 | Pencarian - ID Tenaga Kesehatan | ✅ | 51 | | 5 | Pencarian - ID Pasien | ✅ | 52 | | 6 | Manajemen - Lokasi | ✅ | 53 | | 7 | Manajemen - Suborganisasi | ✅ | 54 | | 8 | Identitas - Anggota Keluarga/Wali | ❌ | 55 | | 9 | Klinis - Pemeriksaan tanda-tanda vital | ⚠️ | 56 | | 10 | Klinis - Pemeriksaan fisik | ❌ | 57 | | 11 | Klinis - Peresepan Obat | ❌ | 58 | | 12 | Klinis - Edukasi | ❌ | 59 | | 13 | Klinis - Pemberian Tindakan | ❌ | 60 | | 14 | Klinis - Prognosis | ❌ | 61 | | 15 | Klinis - Pencatatan Alergi | ❌ | 62 | | 16 | Klinis - Pencatatan Medikasi | ❌ | 63 | | 17 | Klinis - Imunisasi | ❌ | 64 | | 18 | Klinis - Asesmen Risiko | ❌ | 65 | | 19 | Klinis - Asuhan Keperawatan | ❌ | 66 | | 20 | Klinis - Rencana Perawatan | ❌ | 67 | | 21 | Klinis - Rencana Follow-up | ❌ | 68 | | 22 | Logistik - Order Gizi | ❌ | 69 | | 23 | Logistik - Pengaturan Bed | ❌ | 70 | | 24 | Logistik - Manajemen tugas (Task) | ❌ | 71 | | 25 | Logistik - Peralatan (Device) Diagnostik / Terapetik | ❌ | 72 | | 26 | Penunjang - Farmasi Dispense Obat | ❌ | 73 | | 27 | Penunjang - Laboratorium | ❌ | 74 | | 28 | Penunjang - Radiologi (diluar router) | ❌ | 75 | | 29 | Penunjang - Pelaporan genetik | ❌ | 76 | | 30 | Pembiayaan - Billing dan Invoice | ❌ | 77 | | 31 | Pembiayaan - Klaim Swasta | ❌ | 78 | | 32 | Pembiayaan - Klaim BPJS | ❌ | 79 | | 33 | Lain-lain - Kuesioner | ❌ | 80 | 81 | ### Use-case Implementasi 82 | 83 | | | Fitur | Free | 84 | |---|-------------|------| 85 | | 1 | Dasar - Layanan Rawat Jalan | ❌ | 86 | | 2 | Dasar - Layanan Rawat Inap | ❌ | 87 | | 3 | Dasar - Layanan Gawat Darurat | ❌ | 88 | | 4 | Rujukan - Nomor Resep National | ❌ | 89 | | 5 | Rujukan - Sampel laboratorium | ❌ | 90 | | 6 | Rujukan - Sampel Skrining Hipotiroid Kongenital | ❌ | 91 | | 7 | Layanan khusus - Gigi | ❌ | 92 | | 8 | Layanan primer - Antenatal Care | ❌ | 93 | | 9 | Layanan primer - Intranatal Care | ❌ | 94 | | 10 | Layanan primer - Postnatal Care | ❌ | 95 | | 11 | Layanan primer - Gizi | ❌ | 96 | | 12 | Layanan primer - Skrining PTM | ❌ | 97 | | 13 | Layanan primer - Tuberkulosis | ❌ | 98 | | 14 | BGSi - Registri Kanker | ❌ | 99 | | 15 | BGSi - Registri Jantung | ❌ | 100 | | 16 | BGSi - Registri Stroke | ❌ | 101 | | 17 | BGSi - Registri Uronefrologi | ❌ | 102 | | 18 | BGSi - Registri Mata | ❌ | 103 | 104 | ### Penjelasan Simbol: 105 | 106 | - **✅**: Semua metode (GET, POST, PUT) didukung. 107 | - **⚠️**: Dukungan kasus penggunaan sebagian. 108 | - **❌**: Tidak didukung. 109 | 110 | ## 2.9.8 - 2024-11-14 111 | 112 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.9.7...2.9.8 113 | 114 | - @ivanwilliammd Revert mandatory requirement of Patient.address on https://github.com/ivanwilliammd/satusehat-integration/pull/60 115 | 116 | ## 2.9.7 - 2024-11-13 117 | 118 | ### What's Changed 119 | 120 | * Add new physical location type ( Bed ) by @yudistirasd in https://github.com/ivanwilliammd/satusehat-integration/pull/63 121 | 122 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.9.6...2.9.7 123 | 124 | ## 2.9.6 - 2024-11-12 125 | 126 | ### What's Changed 127 | 128 | * Disable required address in Patient by @IrsyadProject in https://github.com/ivanwilliammd/satusehat-integration/pull/60 129 | * Hotfix organization by @yudistirasd ft. @ivanwilliammd in https://github.com/ivanwilliammd/satusehat-integration/pull/62 130 | 131 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.9.5...2.9.6 132 | 133 | ## 2.9.5 - 2024-11-04 134 | 135 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.9.3...2.9.5 136 | 137 | Hotfix by @ivanwilliammd for kode wilayah Indonesia migration --> 2.9.x feat by @IrsyadProject) 138 | 139 | ## 2.9.3 - 2024-11-04 140 | 141 | ### What's Changed 142 | 143 | * Update CSV data kode wilayah dari KEMENDAGRI Tahun 2023, update migration & seeder kode wilayah by @IrsyadProject in https://github.com/ivanwilliammd/satusehat-integration/pull/59 144 | 145 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.9.2...2.9.3 146 | 147 | ## 2.9.2 - 2024-10-10 148 | 149 | ### What's Changed 150 | 151 | * fix: bug migration kode wilayah indonesia by @IrsyadProject in https://github.com/ivanwilliammd/satusehat-integration/pull/58 152 | 153 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.9.1...2.9.2 154 | 155 | ## 2.9.1 - 2024-10-10 156 | 157 | ### What's Changed 158 | 159 | * fix: kode wilayah indonesia by @IrsyadProject in https://github.com/ivanwilliammd/satusehat-integration/pull/57 160 | 161 | ### New Contributors 162 | 163 | * @IrsyadProject made their first contribution in https://github.com/ivanwilliammd/satusehat-integration/pull/57 164 | 165 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.9.0...2.9.1 166 | 167 | ## 2.9.0 - 2024-07-20 168 | 169 | ### What's Changed 170 | 171 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/ivanwilliammd/satusehat-integration/pull/54 172 | * Fixed put function for for encounter, condition, location, observation 173 | 174 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.8.3...2.9.0 175 | 176 | ## 2.8.3 - 2024-07-02 177 | 178 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.8.2...2.8.3 179 | 180 | - Full updated dependencies for Laravel 11 (Illuminate 11) 181 | 182 | ## 2.8.0 - 2024-07-02 183 | 184 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.7.0...2.8.0 185 | 186 | - Add composer.json declaration to support php 8.2+ / 8.3+ 187 | 188 | ## 2.7.0 - 2024-05-09 189 | 190 | ### What's Changed 191 | 192 | * Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/ivanwilliammd/satusehat-integration/pull/47 193 | * Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/ivanwilliammd/satusehat-integration/pull/48 194 | * Fix seach patient by nik by @widialjatsiyah in https://github.com/ivanwilliammd/satusehat-integration/pull/50 195 | 196 | ### New Contributors 197 | 198 | * @widialjatsiyah made their first contribution in https://github.com/ivanwilliammd/satusehat-integration/pull/50 199 | 200 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.6.0...2.7.0 201 | 202 | ## 2.6.0 - 2024-04-10 203 | 204 | ### What's Changed 205 | 206 | * Refactor Exception Handling with some fix by @yudistirasd in https://github.com/ivanwilliammd/satusehat-integration/pull/44 207 | * 43 refactoring exception handlling by @ivanwilliammd in https://github.com/ivanwilliammd/satusehat-integration/pull/45 208 | * fixed OAuthClient.php by @ivanwilliammd in https://github.com/ivanwilliammd/satusehat-integration/pull/46 209 | 210 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.5.1...2.6.0 211 | 212 | ## 2.5.1 - 2024-04-06 213 | 214 | ### What's Changed 215 | 216 | * hotfix multitenancy feature and strictly typed OAuthClient.php by @ivanwilliammd in https://github.com/ivanwilliammd/satusehat-integration/pull/42 217 | 218 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.5.0...2.5.1 219 | 220 | ## 2.5.0 - 2024-04-06 221 | 222 | ### What's Changed 223 | 224 | * Added: API KFA 225 | * Updated : OAuth parameter changed from base_url to fhir_url 226 | * KFA Integration by @yudistirasd in https://github.com/ivanwilliammd/satusehat-integration/pull/38 227 | * 31 api kfa by @ivanwilliammd in https://github.com/ivanwilliammd/satusehat-integration/pull/39 228 | * updated v2.5.0 by @ivanwilliammd in https://github.com/ivanwilliammd/satusehat-integration/pull/40 229 | 230 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.4.0...2.5.0 231 | 232 | ## 2.4.0 - 2024-04-03 233 | 234 | ### What's Changed 235 | 236 | * Update: OAuth2Client, Patient, and Organization by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/36 237 | * Added compatibility for Patient Post with identifier `nik-ibu` 238 | 239 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.3.3...2.4.0 240 | 241 | ## 2.3.3 - 2024-04-02 242 | 243 | ### What's Changed 244 | 245 | * updated json function inside bundle by @ivanwilliammd in https://github.com/ivanwilliammd/satusehat-integration/pull/34 246 | 247 | ### New Contributors 248 | 249 | * @ivanwilliammd made their contribution in https://github.com/ivanwilliammd/satusehat-integration/pull/34 250 | 251 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.3.2...2.3.3 252 | 253 | ## 2.3.2 - 2024-04-02 254 | 255 | ### What's Changed 256 | 257 | * improvement: Condition throw exception error by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/29 258 | 259 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.3.1...2.3.2 260 | 261 | ## 2.3.1 - 2024-03-31 262 | 263 | ### What's Changed 264 | 265 | * fix: Encounter & Condition bundle by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/27 266 | * Linkage of urn:uuid between Encounter & Condition 267 | * Updated wiki 268 | 269 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.3.0...2.3.1 270 | 271 | ## 2.3.0 - Initiation of bundle support - 2024-03-30 272 | 273 | ### What's Changed 274 | 275 | * feat: Encounter & Condition bundle by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/26 276 | 277 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.2.1...2.3.0 278 | 279 | ## v2.3.x : 280 | 281 | - Initiation of bundle support 282 | - Minor Bug fix 283 | 284 | ## v2.2.x : 285 | 286 | - Change test to use PHPUnit 9 for support of 7.4, 8.1, 8.2, 8.3 287 | 288 | ## v2.1.x : 289 | 290 | - Added Kode Wilayah Indonesia (KodWilId) class 291 | - Minor default parameter of `ss_parameter_override` to false in satusehat config file 292 | - Updated .env.example 293 | 294 | ## v2.0.x : 295 | 296 | - Splitted terminology model 297 | - Added new migration database, and seeder 298 | - Expanded Practitioner GET Model 299 | - Updated satusehat config file to support multitenancy with overloading in Controller using `http://github.com/mpociot/teamwork` package 300 | 301 | Example of overloaded BaseController in Laravel 8+: 302 | 303 | ```php 304 | currentTeam= Auth::user()->currentTeam; 321 | $ss_oauth2->satusehat_env = $this->currentTeam->ss_environment; 322 | 323 | // Override construct parameter 324 | if($this->currentTeam){ 325 | if($ss_oauth2->satusehat_env == 'PROD'){ 326 | $ss_oauth2->auth_url = getenv('SATUSEHAT_AUTH_PROD', 'https://api-satusehat.kemkes.go.id/oauth2/v1'); 327 | $ss_oauth2->base_url = getenv('SATUSEHAT_FHIR_PROD', 'https://api-satusehat.kemkes.go.id/fhir-r4/v1'); 328 | $ss_oauth2->client_id = $this->currentTeam->ss_prod_client_id; 329 | $ss_oauth2->client_secret = $this->currentTeam->ss_prod_client_secret; 330 | $ss_oauth2->organization_id = $this->currentTeam->ss_prod_organization_id; 331 | } elseif($ss_oauth2->satusehat_env == 'STG'){ 332 | $ss_oauth2->auth_url = getenv('SATUSEHAT_AUTH_STG', 'https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1'); 333 | $ss_oauth2->base_url = getenv('SATUSEHAT_FHIR_STG', 'https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1'); 334 | $ss_oauth2->client_id = $this->currentTeam->ss_stg_client_id; 335 | $ss_oauth2->client_secret = $this->currentTeam->ss_stg_client_secret; 336 | $ss_oauth2->organization_id = $this->currentTeam->ss_stg_organization_id; 337 | } elseif($ss_oauth2->satusehat_env == 'DEV'){ 338 | $ss_oauth2->auth_url = getenv('SATUSEHAT_AUTH_DEV', 'https://api-satusehat-dev.dto.kemkes.go.id/oauth2/v1'); 339 | $ss_oauth2->base_url = getenv('SATUSEHAT_FHIR_DEV', 'https://api-satusehat-dev.dto.kemkes.go.id/fhir-r4/v1'); 340 | $ss_oauth2->client_id = $this->currentTeam->ss_dev_client_id; 341 | $ss_oauth2->client_secret = $this->currentTeam->ss_dev_client_secret; 342 | $ss_oauth2->organization_id = $this->currentTeam->ss_dev_organization_id; 343 | } else { 344 | return redirect()->route('admin.home')->withDanger('Anda belum menambahkan settingan environment SATUSEHAT pada Database.'); 345 | } 346 | } 347 | 348 | return $ss_oauth2; 349 | } 350 | } 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | ``` 385 | v1.2.x : 386 | 387 | - Backlog Compatilibity with Laravel 8+ (PHP 7.4) / Laravel 9 (PHP 8.0+) / Laravel 10 (PHP 8.1+) 388 | - Bug fixing 389 | - Splitted Encounter statusHistory 390 | - Added functionality of Patient, 391 | - Minor adjustmnent of Organization, Location, and OAuth2Client 392 | - Minor bug fix 393 | 394 | v1.1 : 395 | 396 | - Standardize json() function to result encoded one with pretty print and no escape sequence 397 | - Major functional fix of Encounter & Condition function class. Conversion to ATOM type datetime 398 | - Added beta functionality of Observation 399 | - Fixing inconsistency and function in Condition 400 | - Updated timezone format 401 | 402 | v1.0 : 403 | 404 | - First beta version with PHP Class and consistency update of ICD 10-column migration 405 | - Added faster batch import using csv seeder library 406 | 407 | v0.15 : 408 | 409 | - Last v0 series internally tested for creating OAuth 2 410 | - Shipped basic method for GET by NIK function 411 | - Shipped POST / PUT on FHIR object directly at Encounter, Condition, Organization, Location 412 | 413 | ## 2.2.1 - Ensuring consistency of $this->json() - 2024-03-26 414 | 415 | ### What's Changed 416 | 417 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/ivanwilliammd/satusehat-integration/pull/23 418 | 419 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.2.0...2.2.1 420 | 421 | ## 2.2.0 - Ensuring full compatibility from PHP 7.4, 8.1, 8.2, 8.3 - 2024-03-24 422 | 423 | ### What's Changed 424 | 425 | * [WIP] Backward compatibility to PHP-7.4 by @YogiPristiawan in https://github.com/ivanwilliammd/satusehat-integration/pull/22 426 | 427 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.1.0...2.2.0 428 | 429 | ## 2.1.0 - Minor Adjustment + Kode Wilayah Indonesia Inclusion - 2024-03-22 430 | 431 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/2.0.1...2.1.0 432 | 433 | v2.1.x : 434 | 435 | - Added Kode Wilayah Indonesia (KodWilId) class 436 | - Minor default parameter of `ss_parameter_override` to false in satusehat config file 437 | - Updated .env.example 438 | 439 | ## Major Terminology Update and Multitenancy Support with Overloadding - 2024-03-22 440 | 441 | v2.0.x : 442 | 443 | - Splitted terminology model 444 | - Added new migration database, and seeder 445 | - Expanded Practitioner GET Model 446 | - Updated satusehat config file to support multitenancy with overloading in Controller using `http://github.com/mpociot/teamwork` package 447 | 448 | Example of overloaded BaseController in Laravel 8+: 449 | 450 | ```php 451 | currentTeam= Auth::user()->currentTeam; 468 | $ss_oauth2->satusehat_env = $this->currentTeam->ss_environment; 469 | 470 | // Override construct parameter 471 | if($this->currentTeam){ 472 | if($ss_oauth2->satusehat_env == 'PROD'){ 473 | $ss_oauth2->auth_url = getenv('SATUSEHAT_AUTH_PROD', 'https://api-satusehat.kemkes.go.id/oauth2/v1'); 474 | $ss_oauth2->base_url = getenv('SATUSEHAT_FHIR_PROD', 'https://api-satusehat.kemkes.go.id/fhir-r4/v1'); 475 | $ss_oauth2->client_id = $this->currentTeam->ss_prod_client_id; 476 | $ss_oauth2->client_secret = $this->currentTeam->ss_prod_client_secret; 477 | $ss_oauth2->organization_id = $this->currentTeam->ss_prod_organization_id; 478 | } elseif($ss_oauth2->satusehat_env == 'STG'){ 479 | $ss_oauth2->auth_url = getenv('SATUSEHAT_AUTH_STG', 'https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1'); 480 | $ss_oauth2->base_url = getenv('SATUSEHAT_FHIR_STG', 'https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1'); 481 | $ss_oauth2->client_id = $this->currentTeam->ss_stg_client_id; 482 | $ss_oauth2->client_secret = $this->currentTeam->ss_stg_client_secret; 483 | $ss_oauth2->organization_id = $this->currentTeam->ss_stg_organization_id; 484 | } elseif($ss_oauth2->satusehat_env == 'DEV'){ 485 | $ss_oauth2->auth_url = getenv('SATUSEHAT_AUTH_DEV', 'https://api-satusehat-dev.dto.kemkes.go.id/oauth2/v1'); 486 | $ss_oauth2->base_url = getenv('SATUSEHAT_FHIR_DEV', 'https://api-satusehat-dev.dto.kemkes.go.id/fhir-r4/v1'); 487 | $ss_oauth2->client_id = $this->currentTeam->ss_dev_client_id; 488 | $ss_oauth2->client_secret = $this->currentTeam->ss_dev_client_secret; 489 | $ss_oauth2->organization_id = $this->currentTeam->ss_dev_organization_id; 490 | } else { 491 | return redirect()->route('admin.home')->withDanger('Anda belum menambahkan settingan environment SATUSEHAT pada Database.'); 492 | } 493 | } 494 | 495 | return $ss_oauth2; 496 | } 497 | } 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | ``` 532 | ## 1.2.1 - 2024-03-22 533 | 534 | ### What's Changed 535 | 536 | * feat: setType organization by @yudistirasd in https://github.com/ivanwilliammd/satusehat-integration/pull/14 537 | * #fix FHIR Location by @yudistirasd in https://github.com/ivanwilliammd/satusehat-integration/pull/19 538 | 539 | ### New Contributors 540 | 541 | * @yudistirasd made their first contribution in https://github.com/ivanwilliammd/satusehat-integration/pull/14 542 | 543 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/1.2.0...1.2.1 544 | 545 | ## 1.2.0 - 2024-03-17 546 | 547 | ### What's Changed 548 | 549 | * Bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/ivanwilliammd/satusehat-integration/pull/6 550 | * fix: undefined variable by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/8 551 | * feat: patient by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/9 552 | * fix: organization by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/10 553 | * fix: OAuth2Client by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/11 554 | * split encounter addStatusHistory method by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/12 555 | * Update Encounter, Condition and Observation by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/15 556 | 557 | ### New Contributors 558 | 559 | * @SyaefulKai made their first contribution in https://github.com/ivanwilliammd/satusehat-integration/pull/8 560 | 561 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/1.1...1.2.0 562 | 563 | ## 1.2 - 2024-03-17 564 | 565 | ### What's Changed 566 | 567 | * Bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/ivanwilliammd/satusehat-integration/pull/6 568 | * fix: undefined variable by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/8 569 | * feat: patient by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/9 570 | * fix: organization by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/10 571 | * fix: OAuth2Client by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/11 572 | * split encounter addStatusHistory method by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/12 573 | * Update Encounter, Condition and Observation by @SyaefulKai in https://github.com/ivanwilliammd/satusehat-integration/pull/15 574 | 575 | ### New Contributors 576 | 577 | * @SyaefulKai made their first contribution in https://github.com/ivanwilliammd/satusehat-integration/pull/8 578 | 579 | **Full Changelog**: https://github.com/ivanwilliammd/satusehat-integration/compare/1.1...1.2 580 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute ? 2 | As this is open source project, please kindly support this project by adding feature improvement or bug fixing. Styling fix (comma, delimiter, tab, spacing) will not be considered as contribution and will not considered for merge request. Significant contributor(s) will be mentioned to [credits](README.md#credits).
3 | 4 | 1. Fork this repository 5 | 2. Clone your forked repository 6 | 3. Create new branch 7 | 4. Make changes 8 | 5. Commit and push 9 | 6. Create pull request 10 | -------------------------------------------------------------------------------- /FREEMIUM_COMPARISON.md: -------------------------------------------------------------------------------- 1 | ## Here is the comparison between Free and Premium features 2 | *Last updated: 2025-01-30 12:00 WIB* 3 | 4 | Premium (closed source) package name: 5 | FHIRVel-SS (FHIR Laravel library based on SATUSEHAT Profile) 6 | 7 | ### Bagaimana langkah berlangganan premium 8 | 1. Calon klien mengirimkan [email (klik disini)](mailto:ivan.harsono@ivanmd.id?subject=Subscription%20and%20Support%20for%20SATUSEHAT%20Integration&body=Salam%20sehat%2C%20dr.%20Ivan%0A%0ASaya%20berkebutuhan%20untuk%3A%20(sesuaikan%20dengan%20kebutuhan%20Anda)%0A1.%20Integrasi%20Resource%20%5Bsebutkan%5D%0A2.%20Support%20coding%2Fintegrasi%20database%0A3%20....%0A%0AAnda%20dapat%20mengontak%20saya%20kembali%20melalui%3A%0ANama%3A%20%0ANo.%20Whatsapp%3A%0A%0ATerimakasih%20banyak.) untuk pertanyaan awal, yang akan dibalas dalam 1x24 jam. 9 | 2. Pada sesi awal, akan diadakan sesi **konsultasi diagnostik berbayar** antara konsultan dengan calon klien untuk mengetahui kebutuhan integrasi dan dukungan yang Anda butuhkan. (1x sesi konsultasi diagnostik akan dibayar di muka) 10 | 3. Setelah sesi konsultasi diagnostik berbayar, calon klien akan menerima **proposal penawaran/quotation** yang berisi rincian biaya langganan dan dukungan yang akan diterima dengan skema 11 | - **Skema pemakaian library:** perpetual jual putus/*one-time payment* atau berlangganan tahunan. 12 | - **Dukungan yang diperlukan:** maintenance, konsultasi/pendampingan/training cara integrasi, pelatihan, dan atau dukungan teknis. 13 | 4. Calon klien dapat memilih untuk **setuju** atau **menolak** penawaran. 14 | 5. Apabila calon klien **setuju dengan penawaran**, maka akan diterbitkan **invoice**, dan klien wajib melunasi invoice sebelum waktu tenggat untuk mendapatkan **receipt** (bukti tanda lunas), beserta SLA dan kesepakatan. 15 | 16 | ### Kualitas Kode dan Dukungan 17 | | | Free | Premium | 18 | |----------|------|---------| 19 | | Functional Testing | ⚠️ | ✅ | 20 | | Code Quality Check | ❌ | ✅ | 21 | | Dukungan integrasi | ❌ | ✅ | 22 | | Tanya Jawab | ❌ | ✅ | 23 | | Pelatihan FHIR | ❌ | ✅ | 24 | | Pelatihan Terminologi | ❌ | ✅ | 25 | 26 | 27 | ### Integrasi Terminologi 28 | | | Terminologi | Free | Premium | 29 | |---|-------------|------|---------| 30 | |1 | ICD-10: Diagnosis | ✅ | ✅ | 31 | |2 | ICD-9CM: Procedure | ❌ | ✅ | 32 | |3 | Kode Wilayah Indonesia | ✅ | ✅ | 33 | |4 | KFA API (v2) | ✅ | ✅ | 34 | |5 | LOINC and LOINC Answer: for Lab & Radiology | ❌ | ✅ | 35 | |6 | SNOMED-CT | ❌ | ✅ | 36 | |7 | KPTL (Kode Pembiayaan Tindakan dan Layanan Kesehatan) | ❌ | ✅ | 37 | |8 | Units of Measure | ❌ | ✅ | 38 | |9 | CVX (CDC Vaccine Codes) | ❌ | ✅ | 39 | |10 | Terminology Kemkes (upd. Apr 2023) | ❌ | ✅ | 40 | |11 | FHIR Value Set dan Terminology | ❌ | ✅ | 41 | 42 | ### Integrasi dasar SATUSEHAT 43 | | | Fitur | Free | Premium | 44 | |---|-------------|------|---------| 45 | | 1 | Basic - Otentikasi | ✅ | ✅ | 46 | | 2 | Basic - KYC Centang Biru | ✅ | ✅ | 47 | | 3 | Data - Batch/Bundle Kunjungan Diagnosis | ✅ | ✅ | 48 | | 4 | Pencarian - ID Tenaga Kesehatan | ✅ | ✅ | 49 | | 5 | Pencarian - ID Pasien | ✅ | ✅ | 50 | | 6 | Manajemen - Lokasi | ✅ | ✅ | 51 | | 7 | Manajemen - Suborganisasi | ✅ | ✅ | 52 | | 8 | Identitas - Anggota Keluarga/Wali | ❌ | ✅ | 53 | | 9 | Klinis - Pemeriksaan tanda-tanda vital | ⚠️ | ✅ | 54 | | 10 | Klinis - Pemeriksaan fisik | ❌ | ✅ | 55 | | 11 | Klinis - Peresepan Obat | ❌ | ✅ | 56 | | 12 | Klinis - Edukasi | ❌ | ✅ | 57 | | 13 | Klinis - Pemberian Tindakan | ❌ | ✅ | 58 | | 14 | Klinis - Prognosis | ❌ | ✅ | 59 | | 15 | Klinis - Pencatatan Alergi | ❌ | ✅ | 60 | | 16 | Klinis - Pencatatan Medikasi | ❌ | ✅ | 61 | | 17 | Klinis - Imunisasi | ❌ | ✅ | 62 | | 18 | Klinis - Asesmen Risiko | ❌ | ✅ | 63 | | 19 | Klinis - Asuhan Keperawatan | ❌ | ✅ | 64 | | 20 | Klinis - Rencana Perawatan | ❌ | ✅ | 65 | | 21 | Klinis - Rencana Follow-up | ❌ | ✅ | 66 | | 22 | Logistik - Order Gizi | ❌ | ✅ | 67 | | 23 | Logistik - Pengaturan Bed | ❌ | ✅ | 68 | | 24 | Logistik - Manajemen tugas (Task) | ❌ | ✅ | 69 | | 25 | Logistik - Peralatan (Device) Diagnostik / Terapetik | ❌ | ✅ | 70 | | 26 | Penunjang - Farmasi Dispense Obat | ❌ | ✅ | 71 | | 27 | Penunjang - Laboratorium | ❌ | ✅ | 72 | | 28 | Penunjang - Radiologi (diluar router) | ❌ | ✅ | 73 | | 29 | Penunjang - Pelaporan genetik | ❌ | ✅ | 74 | | 30 | Pembiayaan - Billing dan Invoice | ❌ | 🚀 Coming soon | 75 | | 31 | Pembiayaan - Klaim Swasta | ❌ | 🚀 Coming soon | 76 | | 32 | Pembiayaan - Klaim BPJS | ❌ | 🚀 Coming soon | 77 | | 33 | Lain-lain - Kuesioner | ❌ | ✅ | 78 | 79 | ### Use-case Implementasi 80 | | | Fitur | Free | Premium | 81 | |---|-------------|------|---------| 82 | | 1 | Dasar - Layanan Rawat Jalan | ❌ | ✅ | 83 | | 2 | Dasar - Layanan Rawat Inap | ❌ | ✅ | 84 | | 3 | Dasar - Layanan Gawat Darurat | ❌ | ✅ | 85 | | 4 | Rujukan - Nomor Resep National | ❌ | ⚠️ | 86 | | 5 | Rujukan - Sampel laboratorium | ❌ | ✅ | 87 | | 6 | Rujukan - Sampel Skrining Hipotiroid Kongenital | ❌ | ✅ | 88 | | 7 | Layanan khusus - Gigi | ❌ | ✅ | 89 | | 8 | Layanan primer - Antenatal Care | ❌ | ✅ | 90 | | 9 | Layanan primer - Intranatal Care | ❌ | ✅ | 91 | | 10 | Layanan primer - Postnatal Care | ❌ | ✅ | 92 | | 11 | Layanan primer - Gizi | ❌ | ✅ | 93 | | 12 | Layanan primer - Skrining PTM | ❌ | ✅ | 94 | | 13 | Layanan primer - Tuberkulosis | ❌ | ✅ | 95 | | 14 | BGSi - Registri Kanker | ❌ | ✅ | 96 | | 15 | BGSi - Registri Jantung | ❌ | ✅ | 97 | | 16 | BGSi - Registri Stroke | ❌ | ✅ | 98 | | 17 | BGSi - Registri Uronefrologi | ❌ | ✅ | 99 | | 18 | BGSi - Registri Mata | ❌ | ✅ | 100 | 101 | ### Explanation of the Column: 102 | - **✅**: All methods (GET, POST, PUT) are checked. 103 | - **⚠️**: Partial use-case supported. 104 | - **🚀**: On development, coming soon 105 | - **❌**: Not supported 106 | 107 | [Subscribe now](mailto:ivan.harsono@ivanmd.id?subject=Subscription%20and%20Support%20for%20SATUSEHAT%20Integration&body=Salam%20sehat%2C%20dr.%20Ivan%0A%0ASaya%20berkebutuhan%20untuk%3A%20(sesuaikan%20dengan%20kebutuhan%20Anda)%0A1.%20Integrasi%20Resource%20%5Bsebutkan%5D%0A2.%20Support%20coding%2Fintegrasi%20database%0A3%20....%0A%0AAnda%20dapat%20mengontak%20saya%20kembali%20melalui%3A%0ANama%3A%20%0ANo.%20Whatsapp%3A%0A%0ATerimakasih%20banyak.) for premium features and support. 108 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) ivanwilliammd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build SATUSEHAT FHIR Object in Easy Way 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ivanwilliammd/satusehat-integration.svg?style=flat-square)](https://packagist.org/packages/ivanwilliammd/satusehat-integration) 4 | [![Tests](https://img.shields.io/github/actions/workflow/status/ivanwilliammd/satusehat-integration/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/ivanwilliammd/satusehat-integration/actions/workflows/run-tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/ivanwilliammd/satusehat-integration.svg?style=flat-square)](https://packagist.org/packages/ivanwilliammd/satusehat-integration) 6 | 7 | * This library is open-source and community-maintained, offered as-is without warranty or support from the repository [owner](https://github.com/ivanwilliammd). To enhance sustainability and address the growing demand for support and feature requests, a freemium/subscription-based package was implemented in 2025. For more information, contact: [ivan.harsono@ivanmd.id](mailto:ivan.harsono@ivanmd.id?subject=Subscription%20and%20Support%20for%20SATUSEHAT%20Integration&body=Salam%20sehat%2C%20dr.%20Ivan%0A%0ASaya%20berkebutuhan%20untuk%3A%20(sesuaikan%20dengan%20kebutuhan%20Anda)%0A1.%20Integrasi%20Resource%20%5Bsebutkan%5D%0A2.%20Support%20coding%2Fintegrasi%20database%0A3%20....%0A%0AAnda%20dapat%20mengontak%20saya%20kembali%20melalui%3A%0ANama%3A%20%0ANo.%20Whatsapp%3A%0A%0ATerimakasih%20banyak.) 8 | * Feature comparison (perbandingan fitur Free VS Premium) --> click [here](FREEMIUM_COMPARISON.md) 9 | 10 | ## Introduction 11 | - This unofficial SATUSEHAT FHIR PHP Library to help generate SATUSEHAT FHIR-ready JSON, using profile established by [SATUSEHAT Documentation](https://satusehat.kemkes.go.id/platform/docs). 12 | - This repository is rapidly developing and need help. Please kindly comment in [Issue](https://github.com/ivanwilliammd/satusehat-integration/issues) section to contribute or Sponsor this project. 13 | - Features supported --> see [Wiki](https://github.com/ivanwilliammd/satusehat-integration/wiki/Features) 14 | - Error type from SATUSEHAT --> see [PUBLISHED - Dokumen Kamus Rule Number (Error Code)](https://docs.google.com/spreadsheets/d/1vnYFL2Ho1lICEgWmE2HFwkbEgiRvw1uaYBBW8NvwzjI/edit?gid=927500518#gid=927500518) 15 | 16 | ## SATUSEHAT dissemination summary 17 | - Update (19/9/2024) : Medication is attached to MedicationRequest and MedicationDispense 18 | - Update (21/11/2024): 19 | - SATUSEHAT implements multiple role access with restriction on each API service --> [Resource Access](https://drive.google.com/file/d/1bs8uU_nIuNqHohnRfTvFHx0o2qOgAYabAz0ptUC3w9s/view) 20 | - Data privacy security update, which will censored Patient and Practitioner name 21 | - Patient and Practitioner reference in ```Encounter.subject.display``` and ```Encounter.participant.individual``` must be same with Master Patient Index (Patient GET) and Master Nakes Index (Practitioner GET) 22 | 23 | ## Example Laravel 10 Project with SATUSEHAT Integration 24 | See ```satusehat-integration``` library in action [here](https://github.com/ivanwilliammd/satusehat-laravel-example) 25 | 26 | ## Want to contribute? 27 | - See how to contribute at this [page](CONTRIBUTING.md).
28 | - All contribution will be reviewed by [@ivanwilliammd](https://github.com/ivanwilliammd). Any invalid pull request will be commented, and decided directly whether will need further correction or directly closed as invalid. 29 | 30 | ## Quick Installation 31 | See Quick Installation Instructions [here](https://github.com/ivanwilliammd/satusehat-integration/wiki/Installation)
32 | Feel your first time using this library at Onboarding page [here](https://github.com/ivanwilliammd/satusehat-integration/wiki/Onboarding) 33 | 34 | ## Features 35 | See the feature Wiki page [here](https://github.com/ivanwilliammd/satusehat-integration/wiki/Features) 36 | 37 | ## Full usage guide 38 | Fully documented usage guide could be found on the Usage Wiki section [here](https://github.com/ivanwilliammd/SATUSEHAT-integration/wiki/Usage) 39 | 40 | ## Changelog 41 | 42 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 43 | 44 | ## Credits 45 | 46 | Active contributor (> 1 PR per quarter): 47 | 1. [Dr. dr. Ivan William Harsono, MTI](https://github.com/ivanwilliammd) 48 | 2. ... Looking for volunteer for active contribution ... 49 | 50 | ## License 51 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 52 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported PHP Versions 4 | 5 | To support this library long term, it was considered to use current PHP version as [Digital Transformation Ministry of Health](https://dto.kemkes.go.id) share their library / PHP code snippets. Recommended PHP version : PHP 8+ 6 | 7 | | PHP Version | Supported & Testability | 8 | | ------- | ------------------ | 9 | | 8.3.x | :white_check_mark: | 10 | | 8.2.x | :white_check_mark: | 11 | | 8.1.x | :white_check_mark: | 12 | | 8.0.x | :white_check_mark: | 13 | | 7.4.x | :white_check_mark: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | If you have any code faulty / bug / security vulnerability caused by deprecated library, please kindly open a pull request or issue containing the problem and advice. 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ivanwilliammd/satusehat-integration", 3 | "description": "Build SATUSEHAT FHIR Object in Easy Way", 4 | "keywords": [ 5 | "ivanwilliammd", 6 | "satusehat-integration" 7 | ], 8 | "homepage": "https://github.com/ivanwilliammd/satusehat-integration", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "ivanwilliammd", 13 | "email": "ivanwilliam.md@gmail.com", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.4|^8.0|^8.1|^8.2|^8.3", 19 | "guzzlehttp/guzzle": "^7.8", 20 | "illuminate/config": "^8.0|^9.0|^10.0|^11.0|^12.0", 21 | "illuminate/database": "^8.0|^9.0|^10.0|^11.0|^12.0", 22 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0", 23 | "jeroenzwart/laravel-csv-seeder": "^1.6", 24 | "phpseclib/phpseclib": "^3.0", 25 | "vlucas/phpdotenv": "^5.5" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^9.0" 29 | }, 30 | "suggest": { 31 | "laravel/pint": "Minimalist Opionated Code Formatting Extension for PHP 8.0+", 32 | "pestphp/pest": "Pest PHP Testing Framework for PHP 8.0+" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Satusehat\\Integration\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Satusehat\\Integration\\Tests\\": "tests" 42 | } 43 | }, 44 | "scripts": { 45 | "test": "vendor/bin/phpunit", 46 | "format": "vendor/bin/pint" 47 | }, 48 | "config": { 49 | "sort-packages": true, 50 | "allow-plugins": { 51 | "pestphp/pest-plugin": true, 52 | "phpstan/extension-installer": true 53 | } 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Satusehat\\Integration\\SatusehatIntegrationServiceProvider" 59 | ] 60 | } 61 | }, 62 | "minimum-stability": "dev", 63 | "prefer-stable": true 64 | } 65 | -------------------------------------------------------------------------------- /config/satusehatintegration.php: -------------------------------------------------------------------------------- 1 | 'satusehat_log', 10 | 'token_table_name' => 'satusehat_token', 11 | 12 | 'icd10_table_name' => 'satusehat_icd10', 13 | 14 | 'kode_wilayah_indonesia_table_name' => 'kode_wilayah_indonesia', 15 | 16 | /* 17 | * Override the SATUSEHAT environment, organization, ClientID, and ClientSecret to use 18 | * non environment variable 19 | */ 20 | 21 | 'ss_parameter_override' => false, 22 | 23 | /* 24 | * This is the database connection that will be used by the migration and 25 | * the Activity model shipped with this following Laravel's database.default 26 | * If not set, it will use mysql instead. 27 | */ 28 | 'database_connection_master' => env('DB_CONNECTION_MASTER', 'mysql'), 29 | 'database_connection_satusehat' => env('DB_CONNECTION', 'mysql'), 30 | ]; 31 | -------------------------------------------------------------------------------- /configure.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $option === $default ? strtoupper($option) : $option, 19 | $options, 20 | )); 21 | 22 | $answer = ask("{$question} ({$suggestions})"); 23 | 24 | $validOptions = implode(', ', $options); 25 | 26 | while (! in_array($answer, $options)) { 27 | if ($default && $answer === '') { 28 | $answer = $default; 29 | 30 | break; 31 | } 32 | 33 | writeln(PHP_EOL."Please pick one of the following options: {$validOptions}"); 34 | 35 | $answer = ask("{$question} ({$suggestions})"); 36 | } 37 | 38 | if (! $answer) { 39 | $answer = $default; 40 | } 41 | 42 | return $answer; 43 | } 44 | 45 | function confirm(string $question, bool $default = false): bool 46 | { 47 | $answer = ask($question.' ('.($default ? 'Y/n' : 'y/N').')'); 48 | 49 | if (! $answer) { 50 | return $default; 51 | } 52 | 53 | return strtolower($answer) === 'y'; 54 | } 55 | 56 | function writeln(string $line): void 57 | { 58 | echo $line.PHP_EOL; 59 | } 60 | 61 | function run(string $command): string 62 | { 63 | return trim(shell_exec($command)); 64 | } 65 | 66 | function str_after(string $subject, string $search): string 67 | { 68 | $pos = strrpos($subject, $search); 69 | 70 | if ($pos === false) { 71 | return $subject; 72 | } 73 | 74 | return substr($subject, $pos + strlen($search)); 75 | } 76 | 77 | function slugify(string $subject): string 78 | { 79 | return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $subject), '-')); 80 | } 81 | 82 | function title_case(string $subject): string 83 | { 84 | return str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $subject))); 85 | } 86 | 87 | function replace_in_file(string $file, array $replacements): void 88 | { 89 | $contents = file_get_contents($file); 90 | 91 | file_put_contents( 92 | $file, 93 | str_replace( 94 | array_keys($replacements), 95 | array_values($replacements), 96 | $contents 97 | ) 98 | ); 99 | } 100 | 101 | function removeReadmeParagraphs(string $file): void 102 | { 103 | $contents = file_get_contents($file); 104 | 105 | file_put_contents( 106 | $file, 107 | preg_replace('/.*/s', '', $contents) ?: $contents 108 | ); 109 | } 110 | 111 | function determineSeparator(string $path): string 112 | { 113 | return str_replace('/', DIRECTORY_SEPARATOR, $path); 114 | } 115 | 116 | function replaceForWindows(): array 117 | { 118 | return preg_split('/\\r\\n|\\r|\\n/', run('dir /S /B * | findstr /v /i .git\ | findstr /v /i vendor | findstr /v /i '.basename(__FILE__).' | findstr /r /i /M /F:/ ":author :vendor :package VendorName skeleton vendor_name vendor_slug author@domain.com"')); 119 | } 120 | 121 | function replaceForAllOtherOSes(): array 122 | { 123 | return explode(PHP_EOL, run('grep -E -r -l -i ":author|:vendor|:package|VendorName|skeleton|vendor_name|vendor_slug|author@domain.com" --exclude-dir=vendor ./* ./.github/* | grep -v '.basename(__FILE__))); 124 | } 125 | 126 | function setupTestingLibrary(string $testingLibrary): void 127 | { 128 | if ($testingLibrary === 'pest') { 129 | unlink(__DIR__.'/tests/ExampleTestPhpunit.php'); 130 | unlink(__DIR__.'/.github/workflows/run-tests-phpunit.yml'); 131 | 132 | rename( 133 | from: __DIR__.'/tests/ExampleTestPest.php', 134 | to: __DIR__.'/tests/ExampleTest.php' 135 | ); 136 | 137 | rename( 138 | from: __DIR__.'/.github/workflows/run-tests-pest.yml', 139 | to: __DIR__.'/.github/workflows/run-tests.yml' 140 | ); 141 | 142 | replace_in_file(__DIR__.'/composer.json', [ 143 | ':require_dev_testing' => '"pestphp/pest": "^2.20"', 144 | ':scripts_testing' => '"test": "vendor/bin/pest", 145 | "test-coverage": "vendor/bin/pest --coverage"', 146 | ':plugins_testing' => '"pestphp/pest-plugin": true', 147 | ]); 148 | } elseif ($testingLibrary === 'phpunit') { 149 | unlink(__DIR__.'/tests/ExampleTestPest.php'); 150 | unlink(__DIR__.'/tests/ArchTest.php'); 151 | unlink(__DIR__.'/tests/Pest.php'); 152 | unlink(__DIR__.'/.github/workflows/run-tests-pest.yml'); 153 | 154 | rename( 155 | from: __DIR__.'/tests/ExampleTestPhpunit.php', 156 | to: __DIR__.'/tests/ExampleTest.php' 157 | ); 158 | 159 | rename( 160 | from: __DIR__.'/.github/workflows/run-tests-phpunit.yml', 161 | to: __DIR__.'/.github/workflows/run-tests.yml' 162 | ); 163 | 164 | replace_in_file(__DIR__.'/composer.json', [ 165 | ':require_dev_testing' => '"phpunit/phpunit": "^10.3.2"', 166 | ':scripts_testing' => '"test": "vendor/bin/phpunit", 167 | "test-coverage": "vendor/bin/phpunit --coverage"', 168 | ':plugins_testing,' => '', // We need to remove the comma here as well, since there's nothing to add 169 | ]); 170 | } 171 | } 172 | 173 | function setupCodeStyleLibrary(string $codeStyleLibrary): void 174 | { 175 | if ($codeStyleLibrary === 'pint') { 176 | unlink(__DIR__.'/.github/workflows/fix-php-code-style-issues-cs-fixer.yml'); 177 | 178 | rename( 179 | from: __DIR__.'/.github/workflows/fix-php-code-style-issues-pint.yml', 180 | to: __DIR__.'/.github/workflows/fix-php-code-style-issues.yml' 181 | ); 182 | 183 | replace_in_file(__DIR__.'/composer.json', [ 184 | ':require_dev_codestyle' => '"laravel/pint": "^1.0"', 185 | ':scripts_codestyle' => '"format": "vendor/bin/pint"', 186 | ':plugins_testing' => '', 187 | ]); 188 | 189 | unlink(__DIR__.'/.php-cs-fixer.dist.php'); 190 | } elseif ($codeStyleLibrary === 'cs fixer') { 191 | unlink(__DIR__.'/.github/workflows/fix-php-code-style-issues-pint.yml'); 192 | 193 | rename( 194 | from: __DIR__.'/.github/workflows/fix-php-code-style-issues-cs-fixer.yml', 195 | to: __DIR__.'/.github/workflows/fix-php-code-style-issues.yml' 196 | ); 197 | 198 | replace_in_file(__DIR__.'/composer.json', [ 199 | ':require_dev_codestyle' => '"friendsofphp/php-cs-fixer": "^3.21.1"', 200 | ':scripts_codestyle' => '"format": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes"', 201 | ':plugins_testing' => '', 202 | ]); 203 | } 204 | } 205 | 206 | $gitName = run('git config user.name'); 207 | $authorName = ask('Author name', $gitName); 208 | 209 | $gitEmail = run('git config user.email'); 210 | $authorEmail = ask('Author email', $gitEmail); 211 | 212 | $usernameGuess = explode(':', run('git config remote.origin.url'))[1]; 213 | $usernameGuess = dirname($usernameGuess); 214 | $usernameGuess = basename($usernameGuess); 215 | $authorUsername = ask('Author username', $usernameGuess); 216 | 217 | $vendorName = ask('Vendor name', $authorUsername); 218 | $vendorSlug = slugify($vendorName); 219 | $vendorNamespace = ucwords($vendorName); 220 | $vendorNamespace = ask('Vendor namespace', $vendorNamespace); 221 | 222 | $currentDirectory = getcwd(); 223 | $folderName = basename($currentDirectory); 224 | 225 | $packageName = ask('Package name', $folderName); 226 | $packageSlug = slugify($packageName); 227 | 228 | $className = title_case($packageName); 229 | $className = ask('Class name', $className); 230 | $description = ask('Package description', "This is my package {$packageSlug}"); 231 | 232 | $testingLibrary = askWithOptions( 233 | 'Which testing library do you want to use?', 234 | ['pest', 'phpunit'], 235 | 'phpunit', 236 | ); 237 | 238 | $codeStyleLibrary = askWithOptions( 239 | 'Which code style library do you want to use?', 240 | ['pint', 'cs fixer'], 241 | 'pint', 242 | ); 243 | 244 | writeln('------'); 245 | writeln("Author : {$authorName} ({$authorUsername}, {$authorEmail})"); 246 | writeln("Vendor : {$vendorName} ({$vendorSlug})"); 247 | writeln("Package : {$packageSlug} <{$description}>"); 248 | writeln("Namespace : {$vendorNamespace}\\{$className}"); 249 | writeln("Class name : {$className}"); 250 | writeln("Testing library : {$testingLibrary}"); 251 | writeln("Code style library : {$codeStyleLibrary}"); 252 | writeln('------'); 253 | 254 | writeln('This script will replace the above values in all relevant files in the project directory.'); 255 | 256 | if (! confirm('Modify files?', true)) { 257 | exit(1); 258 | } 259 | 260 | $files = (str_starts_with(strtoupper(PHP_OS), 'WIN') ? replaceForWindows() : replaceForAllOtherOSes()); 261 | 262 | foreach ($files as $file) { 263 | replace_in_file($file, [ 264 | ':author_name' => $authorName, 265 | ':author_username' => $authorUsername, 266 | 'author@domain.com' => $authorEmail, 267 | ':vendor_name' => $vendorName, 268 | ':vendor_slug' => $vendorSlug, 269 | 'VendorName' => $vendorNamespace, 270 | ':package_name' => $packageName, 271 | ':package_slug' => $packageSlug, 272 | 'Skeleton' => $className, 273 | ':package_description' => $description, 274 | ]); 275 | 276 | match (true) { 277 | str_contains($file, determineSeparator('src/SkeletonClass.php')) => rename($file, determineSeparator('./src/'.$className.'Class.php')), 278 | str_contains($file, 'README.md') => removeReadmeParagraphs($file), 279 | default => [], 280 | }; 281 | } 282 | 283 | setupTestingLibrary($testingLibrary); 284 | setupCodeStyleLibrary($codeStyleLibrary); 285 | 286 | confirm('Execute `composer install` and run tests?') && run('composer install && composer test'); 287 | 288 | confirm('Let this script delete itself?', true) && unlink(__FILE__); 289 | -------------------------------------------------------------------------------- /database/migrations/create_kode_wilayah_indonesia_table.php.stub: -------------------------------------------------------------------------------- 1 | create(config('satusehatintegration.kode_wilayah_indonesia_table_name'), function (Blueprint $table) { 17 | $table->bigIncrements('id'); 18 | // Level wilayah (1: Provinsi, 2: Kabupaten, 3: Kecamatan, 4: Desa/Kelurahan) 19 | $table->integer('level')->nullable(); 20 | $table->string('kode_wilayah', 20); // Kode wilayah unik 21 | $table->string('nama_wilayah'); // Nama wilayah 22 | $table->string('parent')->nullable(); // Parent wilayah (kode - nama wilayah) 23 | $table->string('state')->nullable(); // State atau negara bagian 24 | $table->boolean('active')->default(true); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::connection(config('satusehatintegration.database_connection_master'))->dropIfExists(config('satusehatintegration.kode_wilayah_indonesia_table_name')); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/create_satusehat_icd10_table.php.stub: -------------------------------------------------------------------------------- 1 | create(config('satusehatintegration.icd10_table_name'), function (Blueprint $table) { 17 | $table->bigIncrements('id'); 18 | $table->string('icd10_code'); 19 | $table->longText('icd10_en'); 20 | $table->longText('icd10_id')->nullable(); 21 | $table->boolean('active')->default(true); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::connection(config('satusehatintegration.database_connection_master'))->dropIfExists(config('satusehatintegration.icd10_table_name')); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/create_satusehat_log_table.php.stub: -------------------------------------------------------------------------------- 1 | create(config('satusehatintegration.log_table_name'), function (Blueprint $table) { 17 | $table->string('response_id')->nullable(); 18 | $table->string('action'); 19 | $table->string('url'); 20 | $table->json('payload')->nullable(); 21 | $table->json('response'); 22 | $table->string('user_id'); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::connection(config('satusehatintegration.database_connection_satusehat'))->dropIfExists(config('satusehatintegration.log_table_name')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/create_satusehat_token_table.php.stub: -------------------------------------------------------------------------------- 1 | create(config('satusehatintegration.token_table_name'), function (Blueprint $table) { 17 | $table->string('environment'); 18 | $table->string('client_id'); 19 | $table->longText('token'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::connection(config('satusehatintegration.database_connection_satusehat'))->dropIfExists(config('satusehatintegration.token_table_name')); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/seeders/Icd10Seeder.php.stub: -------------------------------------------------------------------------------- 1 | file = base_path().'/database/seeders/csv/icd10.csv'; 16 | $this->tablename = config('satusehatintegration.icd10_table_name'); 17 | $this->delimiter = ';'; 18 | } 19 | 20 | public function run() 21 | { 22 | // Recommended when importing larger CSVs 23 | DB::disableQueryLog(); 24 | 25 | parent::run(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/seeders/KodeWilayahIndonesiaSeeder.php.stub: -------------------------------------------------------------------------------- 1 | file = base_path().'/database/seeders/csv/kode_wilayah_indonesia.csv'; 16 | $this->tablename = config('satusehatintegration.kode_wilayah_indonesia_table_name'); 17 | $this->delimiter = ';'; 18 | } 19 | 20 | public function run() 21 | { 22 | // Recommended when importing larger CSVs 23 | DB::disableQueryLog(); 24 | 25 | parent::run(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resync-tags.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Delete all local tags 3 | for /f "delims=" %%a in ('git tag -l') do git tag -d %%a 4 | 5 | REM Fetch all tags from the remote repository 6 | git fetch --tags 7 | -------------------------------------------------------------------------------- /src/Exception/FHIR/FHIRException.php: -------------------------------------------------------------------------------- 1 | 'Bundle', 12 | 'type' => 'transaction', 13 | 'entry' => [], 14 | ]; 15 | 16 | public $encounter_id; 17 | 18 | public $encounter; 19 | 20 | private function uuidV4() 21 | { 22 | $data = openssl_random_pseudo_bytes(16); 23 | $data[6] = chr(ord($data[6]) & 0x0F | 0x40); 24 | $data[8] = chr(ord($data[8]) & 0x3F | 0x80); 25 | $uuid = bin2hex($data); 26 | $formatted_uuid = sprintf( 27 | '%s-%s-%s-%s-%s', 28 | substr($uuid, 0, 8), 29 | substr($uuid, 8, 4), 30 | substr($uuid, 12, 4), 31 | substr($uuid, 16, 4), 32 | substr($uuid, 20, 12) 33 | ); 34 | 35 | return $formatted_uuid; 36 | } 37 | 38 | public function addEncounter(Encounter $encounter) 39 | { 40 | $this->encounter_id = $this->uuidV4(); 41 | $this->encounter = $encounter; 42 | } 43 | 44 | public function addCondition(Condition $condition) 45 | { 46 | 47 | if (! isset($this->encounter_id)) { 48 | throw new FHIRException('Please call addEncounter method first before addCondition.'); 49 | } 50 | 51 | $condition_uuid = $this->uuidV4(); 52 | 53 | // Membuat referensi condition ke encounter saat ini 54 | $condition->setEncounter($this->encounter_id, '', true); 55 | 56 | // Membuat referensi condition di encounter 57 | $this->encounter->addDiagnosis($condition_uuid, $condition->condition['code']['coding'][0]['code'], '', true); 58 | 59 | if (! isset($this->bundle['entry'][0])) { 60 | $this->bundle['entry'][0] = [ 61 | 'fullUrl' => 'urn:uuid:'.$this->encounter_id, 62 | 'resource' => '', 63 | 'request' => [ 64 | 'method' => 'POST', 65 | 'url' => 'Encounter', 66 | ], 67 | ]; 68 | } 69 | 70 | $this->bundle['entry'][0]['resource'] = json_decode($this->encounter->json()); 71 | 72 | $this->bundle['entry'][] = [ 73 | 'fullUrl' => 'urn:uuid:'.$condition_uuid, 74 | 'resource' => json_decode($condition->json()), 75 | 'request' => [ 76 | 'method' => 'POST', 77 | 'url' => 'Condition', 78 | ], 79 | ]; 80 | } 81 | 82 | public function json() 83 | { 84 | return json_encode($this->bundle, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 85 | } 86 | 87 | public function post() 88 | { 89 | $payload = $this->json(); 90 | [$statusCode, $res] = $this->ss_post('Bundle', $payload); 91 | 92 | return [$statusCode, $res]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/FHIR/Condition.php: -------------------------------------------------------------------------------- 1 | 'Condition']; 12 | 13 | public function addClinicalStatus($clinical_status = 'active') 14 | { 15 | switch ($clinical_status) { 16 | case 'active': 17 | $code = 'active'; 18 | $display = 'Active'; 19 | break; 20 | case 'recurrence': 21 | $code = 'recurrence'; 22 | $display = 'Recurrence'; 23 | break; 24 | case 'inactive': 25 | $code = 'inactive'; 26 | $display = 'Inactive'; 27 | break; 28 | case 'remission': 29 | $code = 'remission'; 30 | $display = 'Remission'; 31 | break; 32 | case 'resolved': 33 | $code = 'resolved'; 34 | $display = 'Resolved'; 35 | break; 36 | default: 37 | $code = 'active'; 38 | $display = 'Active'; 39 | } 40 | 41 | $this->condition['clinicalStatus']['coding'][] = [ 42 | 'system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical', 43 | 'code' => $code, 44 | 'display' => $display, 45 | ]; 46 | } 47 | 48 | public function addCategory($category = 'diagnosis') 49 | { 50 | $category = strtolower($category); 51 | switch ($category) { 52 | case 'diagnosis': 53 | $code = 'encounter-diagnosis'; 54 | $display = 'Encounter Diagnosis'; 55 | break; 56 | case 'keluhan': 57 | $code = 'problem-list-item'; 58 | $display = 'Problem List Item'; 59 | break; 60 | default: 61 | $code = 'encounter-diagnosis'; 62 | $display = 'Encounter Diagnosis'; 63 | } 64 | 65 | $this->condition['category'][] = [ 66 | 'coding' => [ 67 | [ 68 | 'system' => 'http://terminology.hl7.org/CodeSystem/condition-category', 69 | 'code' => $code, 70 | 'display' => $display, 71 | ], 72 | ], 73 | ]; 74 | } 75 | 76 | public function addCode($code = null, $display = null) 77 | { 78 | // Look in database if display is null 79 | $code_check = Icd10::where('icd10_code', $code)->first(); 80 | 81 | // Handling if incomplete code / display 82 | if (! $code_check) { 83 | throw new FHIRException('Kode ICD10 tidak ditemukan'); 84 | } 85 | 86 | $display = $display ? $display : $code_check->icd10_en; 87 | 88 | $this->condition['code']['coding'][] = [ 89 | 'system' => 'http://hl7.org/fhir/sid/icd-10', 90 | 'code' => strtoupper($code), 91 | 'display' => $display, 92 | ]; 93 | } 94 | 95 | public function setSubject($subjectId, $name) 96 | { 97 | $this->condition['subject']['reference'] = 'Patient/'.$subjectId; 98 | $this->condition['subject']['display'] = $name; 99 | } 100 | 101 | public function setEncounter($encounterId, $display = null, $bundle = false) 102 | { 103 | $this->condition['encounter']['reference'] = ($bundle ? 'urn:uuid:' : 'Encounter/').$encounterId; 104 | $this->condition['encounter']['display'] = $display ? $display : 'Kunjungan '.$encounterId; 105 | } 106 | 107 | public function setOnsetDateTime($onset_date_time = null) 108 | { 109 | $this->condition['onsetDateTime'] = $onset_date_time ? 110 | date("Y-m-d\TH:i:sP", strtotime($onset_date_time)) : 111 | date("Y-m-d\TH:i:sP"); 112 | } 113 | 114 | public function setRecordedDate($recorded_date = null) 115 | { 116 | $this->condition['recordedDate'] = $recorded_date ? 117 | date("Y-m-d\TH:i:sP", strtotime($recorded_date)) : 118 | date("Y-m-d\TH:i:sP"); 119 | } 120 | 121 | public function json() 122 | { 123 | // Add default clinical status 124 | if (! array_key_exists('clinicalStatus', $this->condition)) { 125 | $this->addClinicalStatus(); 126 | } 127 | 128 | // Add default category 129 | if (! array_key_exists('category', $this->condition)) { 130 | $this->addCategory(); 131 | } 132 | 133 | // Add default OnsetDateTime 134 | if (! array_key_exists('onsetDateTime', $this->condition)) { 135 | $this->setOnsetDateTime(); 136 | } 137 | 138 | // Add default RecordedDate 139 | if (! array_key_exists('recordedDate', $this->condition)) { 140 | $this->setRecordedDate(); 141 | } 142 | 143 | // Subject is required 144 | if (! array_key_exists('subject', $this->condition)) { 145 | return 'Please use condition->setSubject($subjectId, $name) to pass the data'; 146 | } 147 | 148 | // Encounter is required 149 | if (! array_key_exists('encounter', $this->condition)) { 150 | return 'Please use condition->setEncounter($encounterId) to pass the data'; 151 | } 152 | 153 | // ICD-10 is required 154 | if (! array_key_exists('code', $this->condition)) { 155 | return 'Please use condition->addCode($code, $display) to pass the data'; 156 | } 157 | 158 | return json_encode($this->condition, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 159 | } 160 | 161 | public function post() 162 | { 163 | $payload = $this->json(); 164 | [$statusCode, $res] = $this->ss_post('Condition', $payload); 165 | 166 | return [$statusCode, $res]; 167 | } 168 | 169 | public function put($id) 170 | { 171 | $this->condition['id'] = $id; 172 | 173 | $payload = $this->json(); 174 | [$statusCode, $res] = $this->ss_put('Condition', $id, $payload); 175 | 176 | return [$statusCode, $res]; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/FHIR/Encounter.php: -------------------------------------------------------------------------------- 1 | 'Encounter']; 11 | 12 | public function addRegistrationId($registration_id) 13 | { 14 | $identifier['system'] = 'http://sys-ids.kemkes.go.id/encounter/'.$this->organization_id; 15 | $identifier['value'] = $registration_id; 16 | 17 | $this->encounter['identifier'][] = $identifier; 18 | } 19 | 20 | private function statusHistoryValidate($arr, $status) 21 | { 22 | 23 | $lookingAt = $arr['statusHistory']; 24 | foreach ($lookingAt as $data) { 25 | if ($data['status'] === $status) { 26 | return true; 27 | } 28 | } 29 | 30 | return false; 31 | } 32 | 33 | public function setArrived($timestamp) 34 | { 35 | if (! isset($this->encounter['statusHistory'])) { 36 | $this->encounter['statusHistory'] = []; 37 | } 38 | 39 | $validate = $this->statusHistoryValidate($this->encounter, 'arrived'); 40 | 41 | if ($validate) { 42 | return; 43 | } 44 | 45 | $statusHistory_arrived = [ 46 | 'status' => 'arrived', 47 | 'period' => [ 48 | 'start' => date("Y-m-d\TH:i:sP", strtotime($timestamp)), 49 | ], 50 | ]; 51 | 52 | $this->encounter['status'] = 'arrived'; 53 | $this->encounter['period']['start'] = $statusHistory_arrived['period']['start']; 54 | $this->encounter['statusHistory'][] = $statusHistory_arrived; 55 | } 56 | 57 | public function setInProgress($timestamp_start, $timestamp_end) 58 | { 59 | 60 | if (! isset($this->encounter['statusHistory'])) { 61 | return 'Please use $this->setArrived first'; 62 | } 63 | 64 | $validate = $this->statusHistoryValidate($this->encounter, 'in-progress'); 65 | 66 | if ($validate) { 67 | return; 68 | } 69 | 70 | $atomTimestamp = [ 71 | 'start' => date("Y-m-d\TH:i:sP", strtotime($timestamp_start)), 72 | 'end' => date("Y-m-d\TH:i:sP", strtotime($timestamp_end)), 73 | ]; 74 | 75 | $statusHistory_inprogress = [ 76 | 'status' => 'in-progress', 77 | 'period' => [ 78 | 'start' => $atomTimestamp['start'], 79 | 'end' => $atomTimestamp['end'], 80 | ], 81 | ]; 82 | 83 | $this->encounter['status'] = 'in-progress'; 84 | $this->encounter['period']['end'] = $atomTimestamp['end']; 85 | $this->encounter['statusHistory'][0]['period']['end'] = $atomTimestamp['start']; 86 | $this->encounter['statusHistory'][] = $statusHistory_inprogress; 87 | } 88 | 89 | public function setFinished($timestamp) 90 | { 91 | 92 | if (! isset($this->encounter['statusHistory'])) { 93 | return 'Please use $this->setArrived first'; 94 | } 95 | 96 | $validate = $this->statusHistoryValidate($this->encounter, 'finished'); 97 | 98 | if ($validate) { 99 | return; 100 | } 101 | 102 | $date = date("Y-m-d\TH:i:sP", strtotime($timestamp)); 103 | 104 | $statusHistory_finished = [ 105 | 'status' => 'finished', 106 | 'period' => [ 107 | 'start' => $date, 108 | 'end' => $date, 109 | ], 110 | ]; 111 | 112 | $this->encounter['status'] = 'finished'; 113 | $this->encounter['period']['end'] = $date; 114 | $this->encounter['statusHistory'][] = $statusHistory_finished; 115 | } 116 | 117 | public function setConsultationMethod($consultation_method) 118 | { 119 | switch ($consultation_method) { 120 | case 'RAJAL': 121 | $class_code = 'AMB'; 122 | $class_display = 'ambulatory'; 123 | break; 124 | case 'IGD': 125 | $class_code = 'EMER'; 126 | $class_display = 'emergency'; 127 | break; 128 | case 'RANAP': 129 | $class_code = 'IMP'; 130 | $class_display = 'inpatient encounter'; 131 | break; 132 | case 'HOMECARE': 133 | $class_code = 'HH'; 134 | $class_display = 'home health'; 135 | break; 136 | case 'TELEKONSULTASI': 137 | $class_code = 'TELE'; 138 | $class_display = 'teleconsultation'; 139 | break; 140 | default: 141 | return 'consultation_method is invalid (Choose RAJAL / IGD / RANAP/ HOMECARE / TELEKONSULTASI)'; 142 | } 143 | 144 | $class['code'] = $class_code; 145 | $class['display'] = $class_display; 146 | $class['system'] = 'http://terminology.hl7.org/CodeSystem/v3-ActCode'; 147 | 148 | $this->encounter['class'] = $class; 149 | } 150 | 151 | public function setSubject($subjectId, $name) 152 | { 153 | $this->encounter['subject']['reference'] = 'Patient/'.$subjectId; 154 | $this->encounter['subject']['display'] = $name; 155 | } 156 | 157 | public function addParticipant($participantId, $name, $type = 'ATND', $display = 'attender') 158 | { 159 | $participant['individual']['reference'] = 'Practitioner/'.$participantId; 160 | $participant['individual']['display'] = $name; 161 | $participant['type'][]['coding'][] = [ 162 | 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ParticipationType', 163 | 'code' => $type, 164 | 'display' => $display, 165 | ]; 166 | 167 | $this->encounter['participant'][] = $participant; 168 | } 169 | 170 | public function addLocation($locationId, $name) 171 | { 172 | $location['location']['reference'] = 'Location/'.$locationId; 173 | $location['location']['display'] = $name; 174 | 175 | $this->encounter['location'][] = $location; 176 | } 177 | 178 | public function setServiceProvider() 179 | { 180 | $this->encounter['serviceProvider']['reference'] = 'Organization/'.$this->organization_id; 181 | } 182 | 183 | public function addDiagnosis($id, $code, $display = null, $bundle = false) 184 | { 185 | // Look in database if display is null 186 | $code_check = Icd10::where('icd10_code', $code)->first(); 187 | 188 | // Handling if incomplete code / display 189 | if (! $code_check) { 190 | return 'Kode ICD-10 invalid'; 191 | } 192 | 193 | $display = $display ? $display : $code_check->icd10_en; 194 | 195 | // Create Encounter.diagnosis content 196 | $diagnosis['condition']['reference'] = ($bundle ? 'urn:uuid:' : 'Condition/').$id; 197 | $diagnosis['condition']['display'] = $display; 198 | $diagnosis['use']['coding'][] = [ 199 | 'system' => 'http://terminology.hl7.org/CodeSystem/diagnosis-role', 200 | 'code' => 'DD', 201 | 'display' => 'Discharge diagnosis', 202 | ]; 203 | 204 | // Determine ranking 205 | if (! array_key_exists('diagnosis', $this->encounter)) { 206 | $rank = 1; 207 | } else { 208 | $rank = count($this->encounter['diagnosis']) + 1; 209 | } 210 | $diagnosis['rank'] = $rank; 211 | 212 | $this->encounter['diagnosis'][] = $diagnosis; 213 | } 214 | 215 | public function json() 216 | { 217 | // Status is required 218 | if (! array_key_exists('status', $this->encounter)) { 219 | return 'Please use encounter->statusHistory([timestamp array]) to add the status'; 220 | } 221 | 222 | // Class is required 223 | if (! array_key_exists('class', $this->encounter)) { 224 | return 'Please use encounter->setConsultationMethod($method) to pass the data'; 225 | } 226 | 227 | // Subject is required 228 | if (! array_key_exists('subject', $this->encounter)) { 229 | return 'Please use encounter->setSubject($subjectId, $name) to pass the data'; 230 | } 231 | 232 | // Participant is required 233 | if (! array_key_exists('participant', $this->encounter)) { 234 | return 'Please use encounter->addParticipant($participantId, $name) to pass the data'; 235 | } 236 | 237 | // Location is required 238 | if (! array_key_exists('location', $this->encounter)) { 239 | return 'Please use encounter->addLocation($locationId, $name) to pass the data'; 240 | } 241 | 242 | // Add default ServiceProvider 243 | if (! array_key_exists('serviceProvider', $this->encounter)) { 244 | $this->setServiceProvider(); 245 | } 246 | 247 | return json_encode($this->encounter, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 248 | } 249 | 250 | public function post() 251 | { 252 | $payload = $this->json(); 253 | [$statusCode, $res] = $this->ss_post('Encounter', $payload); 254 | 255 | return [$statusCode, $res]; 256 | } 257 | 258 | public function put($id) 259 | { 260 | $this->encounter['id'] = $id; 261 | 262 | $payload = $this->json(); 263 | [$statusCode, $res] = $this->ss_put('Encounter', $id, $payload); 264 | 265 | return [$statusCode, $res]; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/FHIR/Location.php: -------------------------------------------------------------------------------- 1 | 'Location', 11 | 'status' => 'active', 12 | 'mode' => 'instance', 13 | ]; 14 | 15 | public function addIdentifier($location_identifier) 16 | { 17 | $identifier['system'] = 'http://sys-ids.kemkes.go.id/location/'.$this->organization_id; 18 | $identifier['value'] = $location_identifier; 19 | 20 | $this->location['identifier'][] = $identifier; 21 | } 22 | 23 | public function setName($location_name, $location_description = null) 24 | { 25 | $this->location['name'] = $location_name; 26 | $this->location['description'] = ($location_description == null ? $location_name : $location_description); 27 | } 28 | 29 | public function setStatus($status = 'active') 30 | { 31 | $this->location['status'] = $status ?? 'active'; 32 | } 33 | 34 | public function setOperationalStatus($operational_status = 'U') 35 | { 36 | $operational_status = $operational_status ?? 'U'; 37 | 38 | $display = [ 39 | 'U' => 'Unoccupied', 40 | 'O' => 'Occupied', 41 | 'C' => 'Closed', 42 | 'H' => 'Housekeeping', 43 | 'I' => 'Isolated', 44 | 'K' => 'Contaminated', 45 | ]; 46 | 47 | $this->location['operationalStatus'] = [ 48 | 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0116', 49 | 'code' => $operational_status, 50 | 'display' => $display[$operational_status], 51 | ]; 52 | } 53 | 54 | public function addPhone($phone_number = null) 55 | { 56 | $this->location['telecom'][] = [ 57 | 'system' => 'phone', 58 | 'value' => $phone_number ? $phone_number : getenv('PHONE'), 59 | 'use' => 'work', 60 | ]; 61 | } 62 | 63 | public function addEmail($email = null) 64 | { 65 | $this->location['telecom'][] = [ 66 | 'system' => 'email', 67 | 'value' => $email ? $email : getenv('EMAIL'), 68 | 'use' => 'work', 69 | ]; 70 | } 71 | 72 | public function addUrl($url = null) 73 | { 74 | $this->location['telecom'][] = [ 75 | 'system' => 'url', 76 | 'value' => $url ? $url : getenv('WEBSITE'), 77 | 'use' => 'work', 78 | ]; 79 | } 80 | 81 | public function setAddress($address_line = null, $postal_code = null, $city_name = null, $village_code = null) 82 | { 83 | $this->location['address'] = [ 84 | 'use' => 'work', 85 | 'line' => [ 86 | $address_line ?? getenv('ALAMAT', ''), 87 | ], 88 | 'city' => $city_name ?? getenv('KOTA', ''), 89 | 'postalCode' => $postal_code ?? getenv('KODEPOS', ''), 90 | 'country' => 'ID', 91 | 'extension' => [ 92 | [ 93 | 'url' => 'https://fhir.kemkes.go.id/r4/StructureDefinition/administrativeCode', 94 | 'extension' => [ 95 | [ 96 | 'url' => 'province', 97 | 'valueCode' => $village_code ? substr(str_replace('.', '', $village_code), 0, 2) : getenv('KODE_PROVINSI', ''), 98 | ], 99 | [ 100 | 'url' => 'city', 101 | 'valueCode' => $village_code ? substr(str_replace('.', '', $village_code), 0, 4) : getenv('KODE_KABUPATEN', ''), 102 | ], 103 | [ 104 | 'url' => 'district', 105 | 'valueCode' => $village_code ? substr(str_replace('.', '', $village_code), 0, 6) : getenv('KODE_KECAMATAN', ''), 106 | ], 107 | [ 108 | 'url' => 'village', 109 | 'valueCode' => $village_code ? substr(str_replace('.', '', $village_code), 0, 8) : getenv('KODE_KELURAHAN', ''), 110 | ], 111 | ], 112 | ], 113 | ], 114 | ]; 115 | } 116 | 117 | public function addPhysicalType($physical_type = null) 118 | { 119 | $code = $physical_type ? $physical_type : 'ro'; 120 | $display = [ 121 | 'bu' => 'Building', 122 | 'wi' => 'Wing', 123 | 'co' => 'Corridor', 124 | 'ro' => 'Room', 125 | 've' => 'Vehicle', 126 | 'ho' => 'House', 127 | 'ca' => 'Cabinet', 128 | 'rd' => 'Road', 129 | 'area' => 'Area', 130 | 'bd' => 'Bed', 131 | ]; 132 | 133 | $this->location['physicalType']['coding'][] = [ 134 | 'system' => 'http://terminology.hl7.org/CodeSystem/location-physical-type', 135 | 'code' => $code, 136 | 'display' => $display[$code], 137 | ]; 138 | } 139 | 140 | public function addPosition($latitude = null, $longitude = null) 141 | { 142 | $this->location['position'] = [ 143 | 'latitude' => $latitude ? $latitude : getenv('LATITUDE'), 144 | 'longitude' => $longitude ? $longitude : getenv('LONGITUDE'), 145 | ]; 146 | } 147 | 148 | public function setManagingOrganization($managing_organization = null) 149 | { 150 | $this->location['managingOrganization']['reference'] = 'Organization/'.$managing_organization; 151 | } 152 | 153 | public function setPartOf($part_of = null) 154 | { 155 | $this->location['partOf']['reference'] = 'Location/'.$part_of; 156 | } 157 | 158 | public function json() 159 | { 160 | // Add physicalType if not exist 161 | if (! array_key_exists('physicalType', $this->location)) { 162 | $this->addPhysicalType(); 163 | } 164 | 165 | // // Add latitude & longitude if not exist 166 | // if (!array_key_exists('position', $this->location)) { 167 | // $this->addPosition(); 168 | // } 169 | 170 | // Add default managing organization from parent (registered sarana) 171 | if (! array_key_exists('managingOrganization', $this->location)) { 172 | $this->setManagingOrganization(); 173 | } 174 | 175 | // Name is required 176 | if (! array_key_exists('name', $this->location)) { 177 | return 'Please use location->setName($location_name) to pass the data'; 178 | } 179 | 180 | // Identifier is required 181 | if (! array_key_exists('identifier', $this->location)) { 182 | return 'Please use location->addIdentifier($location_identifier) to pass the data'; 183 | } 184 | 185 | return json_encode($this->location, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 186 | } 187 | 188 | public function post() 189 | { 190 | $payload = $this->json(); 191 | [$statusCode, $res] = $this->ss_post('Location', $payload); 192 | 193 | return [$statusCode, $res]; 194 | } 195 | 196 | public function put($id) 197 | { 198 | $this->location['id'] = $id; 199 | 200 | $payload = $this->json(); 201 | // dd($payload); 202 | [$statusCode, $res] = $this->ss_put('Location', $id, $payload); 203 | 204 | return [$statusCode, $res]; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/FHIR/Observation.php: -------------------------------------------------------------------------------- 1 | 'Observation']; 11 | 12 | /** 13 | * Sets a status to the observation. 14 | * 15 | * @param string $status The status to add. Defaults to "final". 16 | * @return Observation Returns the current instance of the Observation class. 17 | */ 18 | public function setStatus($status = 'final'): Observation 19 | { 20 | switch ($status) { 21 | case 'registered': 22 | $code = 'registered'; 23 | break; 24 | case 'preliminary': 25 | $code = 'preliminary'; 26 | break; 27 | case 'final': 28 | $code = 'final'; 29 | break; 30 | case 'amended': 31 | $code = 'amended'; 32 | break; 33 | case 'corrected': 34 | $code = 'corrected'; 35 | break; 36 | case 'cancelled': 37 | $code = 'cancelled'; 38 | break; 39 | case 'entered-in-error': 40 | $code = 'entered-in-error'; 41 | break; 42 | case 'unknown': 43 | $code = 'unknown'; 44 | break; 45 | default: 46 | $code = 'final'; 47 | } 48 | 49 | $this->observation['status'] = $code; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Adds a category to the observation. 56 | * 57 | * @param string $category the code of the category 58 | * @return Observation The updated observation object. 59 | */ 60 | public function addCategory(string $category): Observation 61 | { 62 | $display = ''; 63 | $code = ''; 64 | switch ($category) { 65 | case 'vital-signs': 66 | $display = 'Vital Signs'; 67 | $code = 'vital-signs'; 68 | break; 69 | } 70 | 71 | // NOTE: we currently only support 'vital-signs' 72 | $this->observation['category'][] = [ 73 | 'coding' => [ 74 | [ 75 | 'system' => 'http://terminology.hl7.org/CodeSystem/observation-category', 76 | 'code' => $code, 77 | 'display' => $display, 78 | ], 79 | ], 80 | ]; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Adds an observation code to the observation. 87 | * If more than one code is added, the last one will be used. 88 | * 89 | * @param string $code The valid observation code to add. 90 | * @return Observation Returns the updated observation object. 91 | */ 92 | public function addCode(string $observationCode): Observation 93 | { 94 | $code = [ 95 | 'system' => 'http://loinc.org', 96 | 'code' => '', 97 | 'display' => '', 98 | ]; 99 | 100 | $display = ''; 101 | $code = ''; 102 | switch ($observationCode) { 103 | case '8480-6': 104 | $display = 'Systolic blood pressure'; 105 | $code = '8480-6'; 106 | break; 107 | case '8462-4': 108 | $display = 'Diastolic blood pressure'; 109 | $code = '8462-4'; 110 | break; 111 | case '8867-4': 112 | $display = 'Heart rate'; 113 | $code = '8867-4'; 114 | break; 115 | case '8310-5': 116 | $display = 'Body temperature'; 117 | $code = '8310-5'; 118 | break; 119 | case '9279-1': 120 | $display = 'Respiratory rate'; 121 | $code = '9279-1'; 122 | break; 123 | } 124 | 125 | $this->observation['code'] = [ 126 | 'coding' => [ 127 | [ 128 | 'system' => 'http://loinc.org', 129 | 'code' => $code, 130 | 'display' => $display, 131 | ], 132 | 133 | ], 134 | ]; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * Sets the subject of the observation. 141 | * 142 | * @param string $subjectId The SATUSEHAT ID of the subject. 143 | * @param string $name The name of the subject. 144 | * @return Observation The current observation instance. 145 | */ 146 | public function setSubject(string $subjectId, string $name): Observation 147 | { 148 | $this->observation['subject'] = [ 149 | 'reference' => "Patient/{$subjectId}", 150 | 'display' => $name, 151 | ]; 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * Sets the performer of the observation. 158 | * 159 | * @param string $performerId The SATUSEHAT ID of the performer. 160 | * @param string $name The name of the performer. 161 | * @return Observation The current observation instance. 162 | */ 163 | public function setPerformer(string $performerId, string $name) 164 | { 165 | $this->observation['performer'][] = [ 166 | 'reference' => "Practitioner/{$performerId}", 167 | 'display' => $name, 168 | ]; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Visit data where observation results are obtained 175 | * 176 | * @param string $encounterId The SATUSEHAT Encounter ID of the encounter. 177 | * @param string $display The display name of the encounter. 178 | */ 179 | public function setEncounter(string $encounterId, ?string $display = null): Observation 180 | { 181 | $this->observation['encounter'] = [ 182 | 'reference' => "Encounter/{$encounterId}", 183 | 'display' => ! empty($display) ? $display : "Kunjungan {$encounterId}", 184 | ]; 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * Returns the JSON representation of the observation. 191 | * 192 | * @return string The JSON representation of the observation. 193 | */ 194 | public function json(): string 195 | { 196 | if (! array_key_exists('status', $this->observation)) { 197 | throw new FHIRMissingProperty('Status is required.'); 198 | } 199 | 200 | if (! array_key_exists('category', $this->observation)) { 201 | throw new FHIRMissingProperty('Category is required.'); 202 | } 203 | 204 | if (! array_key_exists('code', $this->observation)) { 205 | throw new FHIRMissingProperty('Code is required.'); 206 | } 207 | 208 | if (! array_key_exists('subject', $this->observation)) { 209 | throw new FHIRMissingProperty('Subject is required.'); 210 | } 211 | 212 | if (! array_key_exists('encounter', $this->observation)) { 213 | throw new FHIRMissingProperty('Encounter is required.'); 214 | } 215 | 216 | return json_encode($this->observation, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 217 | } 218 | 219 | public function post() 220 | { 221 | $payload = $this->json(); 222 | [$statusCode, $res] = $this->ss_post('Observation', $payload); 223 | 224 | return [$statusCode, $res]; 225 | } 226 | 227 | public function put($id) 228 | { 229 | $this->observation['id'] = $id; 230 | 231 | $payload = $this->json(); 232 | [$statusCode, $res] = $this->ss_put('Observation', $id, $payload); 233 | 234 | return [$statusCode, $res]; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/FHIR/Organization.php: -------------------------------------------------------------------------------- 1 | 'dept', 'display' => 'Hospital Department'], 12 | ['code' => 'prov', 'display' => 'Healthcare Provider'], 13 | ]; 14 | 15 | public array $organization = [ 16 | 'resourceType' => 'Organization', 17 | 'active' => true, 18 | ]; 19 | 20 | public function addIdentifier($organization_identifier) 21 | { 22 | $identifier['system'] = 'http://sys-ids.kemkes.go.id/organization/'.$this->organization_id; 23 | $identifier['value'] = $organization_identifier; 24 | $identifier['use'] = 'official'; 25 | 26 | $this->organization['identifier'][] = $identifier; 27 | } 28 | 29 | public function setName($organization_name) 30 | { 31 | $this->organization['name'] = $organization_name; 32 | } 33 | 34 | public function setOperationalStatus($operational_status = null) 35 | { 36 | $this->organization['extension'][] = [ 37 | 'url' => 'https://fhir.kemkes.go.id/r4/StructureDefinition/operationalStatus', 38 | 'valueCodeableConcept' => [ 39 | 'coding' => [ 40 | [ 41 | 'system' => 'https://fhir.kemkes.go.id/r4/CodeSystem/operational-status', 42 | 'code' => $operational_status ? $operational_status : getenv('OPERATIONAL_STATUS', 'active'), 43 | ], 44 | ], 45 | ], 46 | ]; 47 | } 48 | 49 | public function setPartOf($partOf = null) 50 | { 51 | $this->organization['partOf']['reference'] = 'Organization/'.($partOf ? $partOf : $this->organization_id); 52 | } 53 | 54 | public function setType($type = 'dept') 55 | { 56 | if (! in_array($type, ['dept', 'prov'])) { 57 | throw new FHIRException("Types of organizations currently supported : 'prov' | 'dept' "); 58 | } 59 | 60 | $organizationTypeIndex = array_search('prov', array_column($this->orgType, 'code')); 61 | 62 | $display = $this->orgType[$organizationTypeIndex]['display']; 63 | 64 | $this->organization['type'] = [ 65 | [ 66 | 'coding' => [ 67 | [ 68 | 'system' => 'http://terminology.hl7.org/CodeSystem/organization-type', 69 | 'code' => $type, 70 | 'display' => $display, 71 | ], 72 | ], 73 | ], 74 | ]; 75 | } 76 | 77 | public function addPhone($phone_number = null) 78 | { 79 | $this->organization['telecom'][] = [ 80 | 'system' => 'phone', 81 | 'value' => $phone_number ? $phone_number : getenv('PHONE'), 82 | 'use' => 'work', 83 | ]; 84 | } 85 | 86 | public function addEmail($email = null) 87 | { 88 | $this->organization['telecom'][] = [ 89 | 'system' => 'email', 90 | 'value' => $email ? $email : getenv('EMAIL'), 91 | 'use' => 'work', 92 | ]; 93 | } 94 | 95 | public function addUrl($url = null) 96 | { 97 | $this->organization['telecom'][] = [ 98 | 'system' => 'url', 99 | 'value' => $url ? $url : getenv('WEBSITE'), 100 | 'use' => 'work', 101 | ]; 102 | } 103 | 104 | public function addAddress($address_line = null, $postal_code = null, $city_name = null, $village_code = null) 105 | { 106 | $this->organization['address'][] = [ 107 | 'use' => 'work', 108 | 'type' => 'both', 109 | 'line' => [ 110 | $address_line ?? getenv('ALAMAT', ''), 111 | ], 112 | 'city' => $city_name ?? getenv('KOTA', ''), 113 | 'postalCode' => $postal_code ?? getenv('KODEPOS', ''), 114 | 'country' => 'ID', 115 | 'extension' => [ 116 | [ 117 | 'url' => 'https://fhir.kemkes.go.id/r4/StructureDefinition/administrativeCode', 118 | 'extension' => [ 119 | [ 120 | 'url' => 'province', 121 | 'valueCode' => $village_code ? substr(str_replace('.', '', $village_code), 0, 2) : getenv('KODE_PROVINSI', ''), 122 | ], 123 | [ 124 | 'url' => 'city', 125 | 'valueCode' => $village_code ? substr(str_replace('.', '', $village_code), 0, 4) : getenv('KODE_KABUPATEN', ''), 126 | ], 127 | [ 128 | 'url' => 'district', 129 | 'valueCode' => $village_code ? substr(str_replace('.', '', $village_code), 0, 6) : getenv('KODE_KECAMATAN', ''), 130 | ], 131 | [ 132 | 'url' => 'village', 133 | 'valueCode' => $village_code ? substr(str_replace('.', '', $village_code), 0, 10) : getenv('KODE_KELURAHAN', ''), 134 | ], 135 | ], 136 | ], 137 | ], 138 | ]; 139 | } 140 | 141 | public function json() 142 | { 143 | // Add Organization type 144 | if (! array_key_exists('type', $this->organization)) { 145 | $this->organization['type'][] = [ 146 | 'coding' => [ 147 | [ 148 | 'system' => 'http://terminology.hl7.org/CodeSystem/organization-type', 149 | 'code' => 'dept', 150 | 'display' => 'Hospital Department', 151 | ], 152 | ], 153 | ]; 154 | } 155 | 156 | // Identifier is required 157 | if (! array_key_exists('identifier', $this->organization)) { 158 | return 'Please use organization->addIdentifier($organization_identifier) to pass the data'; 159 | } 160 | 161 | // Name is required 162 | if (! array_key_exists('name', $this->organization)) { 163 | return 'Please use organization->setName($organization_name) to pass the data'; 164 | } 165 | 166 | // Set default Organization part.Of 167 | if (! array_key_exists('partOf', $this->organization)) { 168 | $this->setPartOf(); 169 | } 170 | 171 | // Set default Organization type 172 | if (! array_key_exists('type', $this->organization)) { 173 | $this->setType(); 174 | } 175 | 176 | return json_encode($this->organization, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 177 | } 178 | 179 | public function post() 180 | { 181 | $payload = $this->json(); 182 | [$statusCode, $res] = $this->ss_post('Organization', $payload); 183 | 184 | return [$statusCode, $res]; 185 | } 186 | 187 | public function put($id) 188 | { 189 | $this->organization['id'] = $id; 190 | 191 | $payload = $this->json(); 192 | [$statusCode, $res] = $this->ss_put('Organization', $id, $payload); 193 | 194 | return [$statusCode, $res]; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/FHIR/Patient.php: -------------------------------------------------------------------------------- 1 | 'Patient', 12 | 'meta' => [ 13 | 'profile' => [ 14 | 'https://fhir.kemkes.go.id/r4/StructureDefinition/Patient', 15 | ], 16 | ], 17 | 'active' => true, 18 | ]; 19 | 20 | public function addIdentifier($identifier_type, $identifier_value) 21 | { 22 | if ($identifier_type !== 'nik' && $identifier_type !== 'nik-ibu') { 23 | throw new FHIRException("\$patient->addIdentifier error. Currently, we only support 'nik' or 'nik-ibu' usage."); 24 | } 25 | 26 | $identifier['use'] = 'official'; 27 | $identifier['system'] = 'https://fhir.kemkes.go.id/id/'.$identifier_type; 28 | $identifier['value'] = $identifier_value; 29 | 30 | $this->patient['identifier'][] = $identifier; 31 | } 32 | 33 | public function setName($patient_name) 34 | { 35 | $name['use'] = 'official'; 36 | $name['text'] = $patient_name; 37 | 38 | $this->patient['name'][] = $name; 39 | } 40 | 41 | public function addTelecom($telecom_value, $telecom_system = 'phone', $telecom_use = 'mobile') 42 | { 43 | 44 | $telecom['system'] = $telecom_system; // https://www.hl7.org/fhir/valueset-contact-point-system.html 45 | $telecom['value'] = $telecom_value; 46 | $telecom['use'] = $telecom_use; // https://www.hl7.org/fhir/valueset-contact-point-use.html 47 | 48 | $this->patient['telecom'][] = $telecom; 49 | } 50 | 51 | public function setGender($gender) 52 | { 53 | $this->patient['gender'] = $gender; 54 | } 55 | 56 | public function setBirthDate($date) 57 | { 58 | // YYYY-MM-DD 59 | $this->patient['birthDate'] = $date; 60 | } 61 | 62 | public function setDeceased(bool $bool) 63 | { 64 | $this->patient['deceasedBoolean'] = $bool; 65 | } 66 | 67 | public function setAddress(array $address_detail) 68 | { 69 | $address = [ 70 | 'use' => 'home', 71 | 'line' => [ 72 | $address_detail['address'], 73 | ], 74 | 'city' => $address_detail['city'], 75 | 'postalCode' => $address_detail['postalCode'], 76 | 'country' => $address_detail['country'], 77 | 'extension' => [ 78 | [ 79 | 'url' => 'https://fhir.kemkes.go.id/r4/StructureDefinition/administrativeCode', 80 | 'extension' => [ 81 | [ 82 | 'url' => 'province', 83 | 'valueCode' => substr(str_replace('.', '', $address_detail['provinceCode']), 0, 2), 84 | ], 85 | [ 86 | 'url' => 'city', 87 | 'valueCode' => substr(str_replace('.', '', $address_detail['cityCode']), 0, 4), 88 | ], 89 | [ 90 | 'url' => 'district', 91 | 'valueCode' => substr(str_replace('.', '', $address_detail['districtCode']), 0, 6), 92 | ], 93 | [ 94 | 'url' => 'village', 95 | 'valueCode' => substr(str_replace('.', '', $address_detail['villageCode']), 0, 10), 96 | ], 97 | [ 98 | 'url' => 'rt', 99 | 'valueCode' => $address_detail['rt'], 100 | ], 101 | [ 102 | 'url' => 'rw', 103 | 'valueCode' => $address_detail['rw'], 104 | ], 105 | ], 106 | ], 107 | ], 108 | ]; 109 | 110 | $this->patient['address'][] = $address; 111 | } 112 | 113 | public function setMaritalStatus($marital_status, $marital_code = null, $marital_display = null) 114 | { 115 | /** 116 | * This method can be use either by $patient->setMaritalStatus('Married') 117 | * or if there is no option in switch case 118 | * $patient->setMaritalStatus('', 'UNK', 'Unknown') reference: https://www.hl7.org/fhir/valueset-marital-status.html 119 | */ 120 | $status = strtolower($marital_status); 121 | switch ($status) { 122 | case 'unmarried': 123 | $marital_code = 'U'; 124 | $marital_display = 'Unmarried'; 125 | break; 126 | case 'married': 127 | $marital_code = 'M'; 128 | $marital_display = 'Married'; 129 | break; 130 | case 'divorced': 131 | $marital_code = 'D'; 132 | $marital_display = 'Divorced'; 133 | break; 134 | case 'never': 135 | $marital_code = 'S'; 136 | $marital_display = 'Never Married'; 137 | break; 138 | case 'widowed': 139 | $marital_code = 'W'; 140 | $marital_display = 'Widowed'; 141 | break; 142 | default: 143 | } 144 | 145 | $marital['coding'] = [ 146 | [ 147 | 'system' => 'http://terminology.hl7.org/CodeSystem/v3-MaritalStatus', 148 | 'code' => $marital_code, 149 | 'display' => $marital_display, 150 | ], 151 | ]; 152 | 153 | $marital['text'] = $marital_display; 154 | 155 | $this->patient['maritalStatus'] = $marital; 156 | } 157 | 158 | public function setMultipleBirth($value) 159 | { 160 | if (is_bool($value)) { 161 | $this->patient['multipleBirthBoolean'] = $value; 162 | } elseif (is_int($value)) { 163 | $this->patient['multipleBirthInteger'] = $value; 164 | } 165 | } 166 | 167 | public function setEmergencyContact($name, $phone_number) 168 | { 169 | $emergency['relationship'][] = [ 170 | 'coding' => [ 171 | [ 172 | 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0131', 173 | 'code' => 'C', 174 | ], 175 | ], 176 | ]; 177 | 178 | $emergency['name'] = [ 179 | 'use' => 'official', 180 | 'text' => $name, 181 | ]; 182 | 183 | $emergency['telecom'] = [ 184 | [ 185 | 'system' => 'phone', 186 | 'value' => $phone_number, 187 | 'use' => 'mobile', 188 | ], 189 | ]; 190 | 191 | $this->patient['contact'][] = $emergency; 192 | } 193 | 194 | public function setCommunication($code = 'id-ID', $display = 'Indonesian', bool $preferred = true) 195 | { 196 | $communication['language'] = [ 197 | 'coding' => [ 198 | [ 199 | // https://www.hl7.org/fhir/valueset-languages.html 200 | 'system' => 'urn:ietf:bcp:47', 201 | 'code' => $code, 202 | 'display' => $display, 203 | ], 204 | ], 205 | 'text' => $display, 206 | ]; 207 | $communication['preferred'] = $preferred; 208 | 209 | $this->patient['communication'][] = $communication; 210 | } 211 | 212 | public function setExtension($birth_city, $birth_country, $citizenship) 213 | { 214 | $extension = [ 215 | [ 216 | 'url' => 'https://fhir.kemkes.go.id/r4/StructureDefinition/birthPlace', 217 | 'valueAddress' => [ 218 | 'city' => $birth_city, 219 | 'country' => $birth_country, 220 | ], 221 | ], 222 | [ 223 | 'url' => 'https://fhir.kemkes.go.id/r4/StructureDefinition/citizenshipStatus', 224 | 'valueCode' => $citizenship, 225 | ], 226 | ]; 227 | $this->patient['extension'] = $extension; 228 | } 229 | 230 | public function json() 231 | { 232 | 233 | // identifier is required 234 | if (! array_key_exists('identifier', $this->patient)) { 235 | throw new FHIRException('Please use patient->addIdentifier($identifier_type, $identifier_value) to pass the data'); 236 | } 237 | 238 | // Name is required 239 | if (! array_key_exists('name', $this->patient)) { 240 | throw new FHIRException('Please use patient->setName($organization_name) to pass the data'); 241 | } 242 | 243 | // Address is required 244 | if (! array_key_exists('address', $this->patient)) { 245 | throw new FHIRException('Please use patient->setAddress($address_detail) to pass the data'); 246 | } 247 | 248 | // Telecom is required 249 | if (! array_key_exists('telecom', $this->patient)) { 250 | throw new FHIRException('Please use patinet->addTelecom("phone_number") to pass the data'); 251 | } 252 | 253 | // Multiple birth is required 254 | if (! array_key_exists('multipleBirthInteger', $this->patient)) { 255 | throw new FHIRException('Please use patient->setMultipleBirth({integer/boolean}) to pass the data'); 256 | } 257 | 258 | return json_encode($this->patient, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); 259 | } 260 | 261 | public function post() 262 | { 263 | $payload = $this->json(); 264 | [$statusCode, $res] = $this->ss_post('Patient', $payload); 265 | 266 | return [$statusCode, $res]; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/FHIR/Practitioner.php: -------------------------------------------------------------------------------- 1 | get_by_nik('Practitioner', $nik); 14 | 15 | if ($statusCode != 200) { 16 | return null; 17 | } 18 | 19 | $this->practitioner = $res->entry ? $res->entry[0]->resource : null; 20 | 21 | return $this->practitioner; 22 | } 23 | 24 | public function getId() 25 | { 26 | // If practitioner is not found, return null 27 | return ! $this->practitioner ? null : $this->practitioner->id; 28 | } 29 | 30 | public function getGender() 31 | { 32 | // If practitioner is not found, return null 33 | return ! $this->practitioner ? null : $this->practitioner->gender; 34 | } 35 | 36 | public function getBirthDate() 37 | { 38 | // If practitioner is not found, return null 39 | return ! $this->practitioner ? null : $this->practitioner->birthDate; 40 | } 41 | 42 | public function getName() 43 | { 44 | // If practitioner is not found, return null 45 | return ! $this->practitioner ? null : $this->practitioner->name[0]->text; 46 | } 47 | 48 | public function getQualificationValue() 49 | { 50 | // If practitioner is not found, return null 51 | return ! $this->practitioner ? null : $this->practitioner->qualification[0]->identifier[0]->value; 52 | } 53 | 54 | public function getAddressLine() 55 | { 56 | // If practitioner is not found, return null 57 | return ! $this->practitioner ? null : $this->practitioner->address[0]->line[0]; 58 | } 59 | 60 | public function getCity() 61 | { 62 | // If practitioner is not found, return null 63 | return ! $this->practitioner ? null : $this->practitioner->address[0]->extension[0]->extension[1]->valueCode; 64 | } 65 | 66 | public function getVillage() 67 | { 68 | // If practitioner is not found, return null 69 | return ! $this->practitioner ? null : $this->practitioner->address[0]->extension[0]->extension[3]->valueCode; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/KYC.php: -------------------------------------------------------------------------------- 1 | OPENSSL_KEYTYPE_RSA, 18 | 'private_key_bits' => 2048, 19 | ]; 20 | 21 | $keyPair = openssl_pkey_new($config); 22 | 23 | // Extract the public key 24 | $publicKey = openssl_pkey_get_details($keyPair)['key']; 25 | 26 | // Export the private key 27 | openssl_pkey_export($keyPair, $privateKey); 28 | 29 | return [ 30 | 'publicKey' => $publicKey, 31 | 'privateKey' => $privateKey, 32 | ]; 33 | } 34 | 35 | // Cryptography 36 | public function importRsaKey($pem) 37 | { 38 | // Fetch the part of the PEM string between header and footer 39 | $pemHeader = '-----BEGIN PUBLIC KEY-----'; 40 | $pemFooter = '-----END PUBLIC KEY-----'; 41 | $pemContents = substr($pem, strlen($pemHeader), strlen($pem) - strlen($pemFooter)); 42 | 43 | // Base64 decode the string to get the binary data 44 | $binaryDerString = base64_decode($pemContents); 45 | 46 | // Save the binary DER data to a temporary file 47 | $tempFile = tempnam(sys_get_temp_dir(), 'rsa_key'); 48 | file_put_contents($tempFile, $binaryDerString); 49 | 50 | // Import the RSA key using openssl 51 | $key = openssl_pkey_get_public('file://'.$tempFile); 52 | 53 | // Generate the key details for encryption 54 | $keyDetails = openssl_pkey_get_details($key); 55 | 56 | // Clean up the temporary file 57 | unlink($tempFile); 58 | 59 | return $key; 60 | } 61 | 62 | public function generateSymmetricKey() 63 | { 64 | // Generate a random key using OpenSSL 65 | $cryptoStrong = true; 66 | $key = openssl_random_pseudo_bytes(32, $cryptoStrong); 67 | 68 | if ($cryptoStrong !== true) { 69 | // Error occurred during key generation 70 | return null; 71 | } 72 | 73 | // Return the generated key 74 | return $key; 75 | } 76 | 77 | public function generateRSAKeyPair() 78 | { 79 | // Generate private key 80 | $privateKeyConfig = [ 81 | 'digest_alg' => 'sha256', 82 | 'private_key_bits' => 2048, 83 | 'private_key_type' => OPENSSL_KEYTYPE_RSA, 84 | ]; 85 | $privateKeyResource = openssl_pkey_new($privateKeyConfig); 86 | 87 | // Extract the public key from the private key 88 | $privateKeyDetails = openssl_pkey_get_details($privateKeyResource); 89 | $publicKey = $privateKeyDetails['key']; 90 | 91 | // Prepare the result 92 | $result = [ 93 | 'privateKey' => $privateKeyResource, 94 | 'publicKey' => $publicKey, 95 | ]; 96 | 97 | return $result; 98 | } 99 | 100 | public function formatMessage($data) 101 | { 102 | $dataAsBase64 = chunk_split(base64_encode($data)); 103 | 104 | return "-----BEGIN ENCRYPTED MESSAGE-----\r\n{$dataAsBase64}-----END ENCRYPTED MESSAGE-----"; 105 | } 106 | 107 | public function aesEncrypt($data, $symmetricKey) 108 | { 109 | $cipher = 'aes-256-gcm'; 110 | $ivLength = 12; 111 | $tag = ''; 112 | $iv = ''; 113 | 114 | // Generate random IV 115 | if (function_exists('random_bytes')) { 116 | $iv = random_bytes($ivLength); 117 | } elseif (function_exists('openssl_random_pseudo_bytes')) { 118 | $iv = openssl_random_pseudo_bytes($ivLength); 119 | } else { 120 | // Fallback if random bytes generation is not available 121 | $iv = ''; 122 | for ($i = 0; $i < $ivLength; $i++) { 123 | $iv .= chr(mt_rand(0, 255)); 124 | } 125 | } 126 | 127 | // $cipher = new AES(AES::MODE_GCM); 128 | $cipher = new AES('gcm'); 129 | $cipher->setKeyLength(256); 130 | $cipher->setKey($symmetricKey); 131 | $cipher->setNonce($iv); 132 | 133 | $ciphertext = $cipher->encrypt($data); 134 | $tag = $cipher->getTag(); 135 | 136 | // Concatenate the IV, ciphertext, and tag 137 | $encryptedData = $iv.$ciphertext.$tag; 138 | 139 | return $encryptedData; 140 | } 141 | 142 | public function aesDecrypt($encryptedData, $symmetricKey) 143 | { 144 | $cipher = 'aes-256-gcm'; 145 | $ivLength = 12; 146 | 147 | // Extract IV and encrypted bytes 148 | $iv = substr($encryptedData, 0, $ivLength); 149 | $encryptedBytes = substr($encryptedData, $ivLength); 150 | 151 | $ivlen = openssl_cipher_iv_length($cipher); 152 | $tag_length = 16; 153 | $iv = substr($encryptedData, 0, $ivlen); 154 | $tag = substr($encryptedData, -$tag_length); 155 | $ciphertext = substr($encryptedData, $ivlen, -$tag_length); 156 | 157 | $ciphertext_raw = openssl_decrypt($ciphertext, $cipher, $symmetricKey, OPENSSL_NO_PADDING, $iv, $tag); 158 | 159 | return $ciphertext_raw; 160 | 161 | // Decrypt the data 162 | $decryptedData = openssl_decrypt( 163 | $encryptedBytes, 164 | $cipher, 165 | $symmetricKey, 166 | OPENSSL_RAW_DATA, 167 | $iv 168 | ); 169 | 170 | return $decryptedData; 171 | } 172 | 173 | public function encryptMessage($message, $pubPEM) 174 | { 175 | // Generate a symmetric key 176 | $aesKey = $this->generateSymmetricKey(); // Generate a 256-bit key (32 bytes) 177 | 178 | $serverKey = PublicKeyLoader::load($pubPEM); 179 | $serverKey = $serverKey->withPadding(RSA::ENCRYPTION_OAEP); 180 | $wrappedAesKey = $serverKey->encrypt($aesKey); 181 | 182 | // echo ($wrappedAesKey); 183 | 184 | // Encrypt the message using the generated AES key 185 | $encryptedMessage = $this->aesEncrypt($message, $aesKey); 186 | 187 | // Combine wrapped AES key and encrypted message 188 | $payload = $wrappedAesKey.$encryptedMessage; 189 | 190 | return $this->formatMessage($payload); 191 | } 192 | 193 | public function decryptMessage($message, $privateKey) 194 | { 195 | $beginTag = '-----BEGIN ENCRYPTED MESSAGE-----'; 196 | $endTag = '-----END ENCRYPTED MESSAGE-----'; 197 | 198 | // Fetch the part of the PEM string between beginTag and endTag 199 | $messageContents = substr( 200 | $message, 201 | strlen($beginTag) + 1, 202 | strlen($message) - strlen($endTag) - strlen($beginTag) - 2 203 | ); 204 | 205 | // Base64 decode the string to get the binary data 206 | $binaryDerString = base64_decode($messageContents); 207 | 208 | // Split the binary data into wrapped key and encrypted message 209 | $wrappedKeyLength = 256; 210 | $wrappedKey = substr($binaryDerString, 0, $wrappedKeyLength); 211 | $encryptedMessage = substr($binaryDerString, $wrappedKeyLength); 212 | 213 | // Unwrap the key using RSA private key 214 | // $unwrappedKey = unwrapKey($wrappedKey, $privateKey); 215 | 216 | // $key = new RSA(); 217 | 218 | // $key->loadKey($privateKey); 219 | $key = PublicKeyLoader::load($privateKey); 220 | // $key = $key->withPadding(RSA::ENCRYPTION_OAEP); 221 | $aesKey = $key->decrypt($wrappedKey); 222 | 223 | // Decrypt the encrypted message using the unwrapped key 224 | $decryptedMessage = $this->aesDecrypt($encryptedMessage, $aesKey); 225 | 226 | return $decryptedMessage; 227 | } 228 | 229 | public function generateUrl($agen, $nik_agen) 230 | { 231 | $keyPair = $this->generateKey(); 232 | $publicKey = $keyPair['publicKey']; 233 | $privateKey = $keyPair['privateKey']; 234 | 235 | $accessToken = $this->token(); 236 | 237 | // Set the API URL and authentication token 238 | $apiUrl = 'https://api-satusehat.kemkes.go.id/kyc/v1/generate-url'; 239 | 240 | $pubPEM = '-----BEGIN PUBLIC KEY----- 241 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxLwvebfOrPLIODIxAwFp 242 | 4Qhksdtn7bEby5OhkQNLTdClGAbTe2tOO5Tiib9pcdruKxTodo481iGXTHR5033I 243 | A5X55PegFeoY95NH5Noj6UUhyTFfRuwnhtGJgv9buTeBa4pLgHakfebqzKXr0Lce 244 | /Ff1MnmQAdJTlvpOdVWJggsb26fD3cXyxQsbgtQYntmek2qvex/gPM9Nqa5qYrXx 245 | 8KuGuqHIFQa5t7UUH8WcxlLVRHWOtEQ3+Y6TQr8sIpSVszfhpjh9+Cag1EgaMzk+ 246 | HhAxMtXZgpyHffGHmPJ9eXbBO008tUzrE88fcuJ5pMF0LATO6ayXTKgZVU0WO/4e 247 | iQIDAQAB 248 | -----END PUBLIC KEY-----'; 249 | 250 | // Set the request data 251 | $data = [ 252 | 'agent_name' => $agen, 253 | 'agent_nik' => $nik_agen, 254 | 'public_key' => $publicKey, 255 | ]; 256 | 257 | $jsonData = json_encode($data); 258 | 259 | $encryptedPayload = $this->encryptMessage($jsonData, $pubPEM); 260 | 261 | // Initialize cURL 262 | $ch = curl_init(); 263 | 264 | // Set the cURL options 265 | curl_setopt($ch, CURLOPT_URL, $apiUrl); 266 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 267 | curl_setopt($ch, CURLOPT_POST, true); 268 | curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedPayload); 269 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 270 | 'Content-Type: text/plain', 271 | 'Authorization: Bearer '.$accessToken, 272 | ]); 273 | 274 | // Execute the request 275 | $response = curl_exec($ch); 276 | 277 | // Check for cURL errors 278 | if (curl_errno($ch)) { 279 | echo 'cURL error: '.curl_error($ch); 280 | } 281 | 282 | // Close cURL 283 | curl_close($ch); 284 | 285 | // Output the response 286 | return $this->decryptMessage($response, $privateKey); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/Models/SatusehatLog.php: -------------------------------------------------------------------------------- 1 | connection)) { 28 | $this->setConnection(config('satusehatintegration.database_connection_satusehat')); 29 | } 30 | 31 | if (! isset($this->table)) { 32 | $this->setTable(config('satusehatintegration.log_table_name')); 33 | } 34 | 35 | parent::__construct($attributes); 36 | } 37 | 38 | protected $primaryKey = 'created_at'; 39 | 40 | public $incrementing = false; 41 | 42 | protected $casts = ['response_id' => 'string', 'action' => 'string', 'url' => 'string', 43 | 'payload' => 'array', 'response' => 'array', 'user_id' => 'string']; 44 | } 45 | -------------------------------------------------------------------------------- /src/Models/SatusehatToken.php: -------------------------------------------------------------------------------- 1 | connection)) { 22 | $this->setConnection(config('satusehatintegration.database_connection_satusehat')); 23 | } 24 | 25 | if (! isset($this->table)) { 26 | $this->setTable(config('satusehatintegration.token_table_name')); 27 | } 28 | 29 | parent::__construct($attributes); 30 | } 31 | 32 | protected $primaryKey = 'token'; 33 | 34 | public $incrementing = false; 35 | 36 | protected $casts = ['environment' => 'string', 'client_id' => 'string', 'token' => 'string']; 37 | } 38 | -------------------------------------------------------------------------------- /src/OAuth2Client.php: -------------------------------------------------------------------------------- 1 | 401, 39 | 'res' => 'Unauthorized. Token not found', 40 | ]; 41 | 42 | public function __construct() 43 | { 44 | $dotenv = Dotenv::createUnsafeImmutable(getcwd()); 45 | $dotenv->safeLoad(); 46 | 47 | $this->override = config('satusehatintegration.ss_parameter_override'); 48 | 49 | $this->satusehat_env = $this->override ? null : getenv('SATUSEHAT_ENV'); 50 | 51 | if ($this->satusehat_env == 'PROD') { 52 | $this->base_url = getenv('SATUSEHAT_BASE_URL_PROD') ?: 'https://api-satusehat.kemkes.go.id'; 53 | $this->client_id = getenv('CLIENTID_PROD'); 54 | $this->client_secret = getenv('CLIENTSECRET_PROD'); 55 | $this->organization_id = getenv('ORGID_PROD'); 56 | } elseif ($this->satusehat_env == 'STG') { 57 | $this->base_url = getenv('SATUSEHAT_BASE_URL_STG') ?: 'https://api-satusehat-stg.dto.kemkes.go.id'; 58 | $this->client_id = getenv('CLIENTID_STG'); 59 | $this->client_secret = getenv('CLIENTSECRET_STG'); 60 | $this->organization_id = getenv('ORGID_STG'); 61 | } elseif ($this->satusehat_env == 'DEV') { 62 | $this->base_url = getenv('SATUSEHAT_BASE_URL_DEV') ?: 'https://api-satusehat-dev.dto.kemkes.go.id'; 63 | $this->client_id = getenv('CLIENTID_DEV'); 64 | $this->client_secret = getenv('CLIENTSECRET_DEV'); 65 | $this->organization_id = getenv('ORGID_DEV'); 66 | } 67 | 68 | if (empty($this->satusehat_env) && ! $this->override) { 69 | throw new OAuth2ClientException('SATUSEHAT environment is missing'); 70 | } 71 | 72 | if (! in_array($this->satusehat_env, ['DEV', 'STG', 'PROD']) && ! $this->override) { 73 | throw new OAuth2ClientException('SATUSEHAT environment invalid, supported (DEV, STG, PROD). '.$this->satusehat_env.' given.'); 74 | } 75 | 76 | if ($this->satusehat_env == 'DEV' && (empty($this->client_id) || empty($this->client_secret || empty($this->organization_id))) && ! $this->override) { 77 | throw new OAuth2ClientException('SATUSEHAT environment defined as DEV, but CLIENTID_DEV / CLIENTSECRET_DEV / ORGID_DEV not set'); 78 | } 79 | 80 | if ($this->satusehat_env == 'STG' && (empty($this->client_id) || empty($this->client_secret || empty($this->organization_id))) && ! $this->override) { 81 | throw new OAuth2ClientException('SATUSEHAT environment defined as STG, but CLIENTID_STG / CLIENTSECRET_STG / ORGID_STG not set'); 82 | } 83 | 84 | if ($this->satusehat_env == 'PROD' && (empty($this->client_id) || empty($this->client_secret || empty($this->organization_id))) && ! $this->override) { 85 | throw new OAuth2ClientException('SATUSEHAT environment defined as PROD, but CLIENTID_PROD / CLIENTSECRET_PROD / ORGID_PROD not set'); 86 | } 87 | 88 | $this->base_url = $this->override ? null : $this->base_url; 89 | 90 | $authEndpoint = getenv('SATUSEHAT_AUTH_ENDPOINT') ?: '/oauth2/v1'; 91 | $fhirEndpoint = getenv('SATUSEHAT_FHIR_ENDPOINT') ?: '/fhir-r4/v1'; 92 | 93 | // // untuk handle versioning endpoint 94 | $this->auth_url = $this->base_url.$authEndpoint; 95 | $this->fhir_url = $this->base_url.$fhirEndpoint; 96 | 97 | if (! $this->override && $this->organization_id == null) { 98 | return 'Add your organization_id at environment first'; 99 | } 100 | } 101 | 102 | public function token() 103 | { 104 | $token = SatusehatToken::where('environment', $this->satusehat_env)->where('client_id', $this->client_id)->orderBy('created_at', 'desc') 105 | ->where('created_at', '>', now()->subMinutes(50))->first(); 106 | 107 | if ($token) { 108 | return $token->token; 109 | } 110 | 111 | $client = new Client; 112 | 113 | $headers = [ 114 | 'Content-Type' => 'application/x-www-form-urlencoded', 115 | ]; 116 | $options = [ 117 | 'form_params' => [ 118 | 'client_id' => $this->client_id, 119 | 'client_secret' => $this->client_secret, 120 | ], 121 | ]; 122 | 123 | // Create session 124 | $url = $this->auth_url.'/accesstoken?grant_type=client_credentials'; 125 | $request = new Request('POST', $url, $headers); 126 | 127 | try { 128 | $res = $client->sendAsync($request, $options)->wait(); 129 | $contents = json_decode($res->getBody()->getContents()); 130 | 131 | if (isset($contents->access_token)) { 132 | SatusehatToken::create([ 133 | 'environment' => $this->satusehat_env, 134 | 'client_id' => $this->client_id, 135 | 'token' => $contents->access_token, 136 | ]); 137 | 138 | return $contents->access_token; 139 | } else { 140 | return $this->respondError($this->oauth2_error); 141 | } 142 | } catch (ClientException $e) { 143 | // error. 144 | $res = json_decode($e->getResponse()->getBody()->getContents()); 145 | $issue_information = $res->issue[0]->details->text; 146 | 147 | $this->log($issue_information, 'POST Token', $url, null, (array) $res); 148 | 149 | return $issue_information; 150 | } 151 | } 152 | 153 | public function log($id, $action, $url, $payload, $response) 154 | { 155 | $status = new SatusehatLog; 156 | $status->response_id = $id; 157 | $status->action = $action; 158 | $status->url = $url; 159 | $status->payload = $payload; 160 | $status->response = $response; 161 | $status->user_id = auth()->user() ? auth()->user()->id : 'Cron Job'; 162 | $status->save(); 163 | } 164 | 165 | public function respondError($message) 166 | { 167 | $statusCode = $message['statusCode']; 168 | $res = $message['res']; 169 | 170 | return [$statusCode, $res]; 171 | } 172 | 173 | public function get_by_id($resource, $id) 174 | { 175 | $access_token = $this->token(); 176 | 177 | if (! isset($access_token)) { 178 | return $this->respondError($this->oauth2_error); 179 | } 180 | 181 | $client = new Client; 182 | $headers = [ 183 | 'Authorization' => 'Bearer '.$access_token, 184 | ]; 185 | 186 | $url = $this->fhir_url.'/'.$resource.'/'.$id; 187 | $request = new Request('GET', $url, $headers); 188 | 189 | try { 190 | $res = $client->sendAsync($request)->wait(); 191 | $statusCode = $res->getStatusCode(); 192 | $response = json_decode($res->getBody()->getContents()); 193 | 194 | if ($response->resourceType == 'OperationOutcome' | $response->total == 0) { 195 | $id = 'Error '.$statusCode; 196 | } 197 | $this->log($id, 'GET', $url, null, (array) $response); 198 | 199 | return [$statusCode, $response]; 200 | } catch (ClientException $e) { 201 | $statusCode = $e->getResponse()->getStatusCode(); 202 | $res = json_decode($e->getResponse()->getBody()->getContents()); 203 | 204 | $this->log('Error '.$statusCode, 'GET', $url, null, (array) $res); 205 | 206 | return [$statusCode, $res]; 207 | } 208 | } 209 | 210 | public function get_by_nik($resource, $nik) 211 | { 212 | $access_token = $this->token(); 213 | 214 | if (! isset($access_token)) { 215 | return $this->respondError($this->oauth2_error); 216 | } 217 | 218 | $client = new Client; 219 | $headers = [ 220 | 'Authorization' => 'Bearer '.$access_token, 221 | ]; 222 | 223 | $url = $this->fhir_url.'/'.$resource.'?identifier=https://fhir.kemkes.go.id/id/nik|'.$nik; 224 | $request = new Request('GET', $url, $headers); 225 | 226 | try { 227 | $res = $client->sendAsync($request)->wait(); 228 | $statusCode = $res->getStatusCode(); 229 | $response = json_decode($res->getBody()->getContents()); 230 | 231 | if ($response->resourceType == 'OperationOutcome' | $response->total == 0) { 232 | $id = 'Not Found'; 233 | } else { 234 | $id = $response->entry['0']->resource->id; 235 | } 236 | $this->log($id, 'GET', $url, null, (array) $response); 237 | 238 | return [$statusCode, $response]; 239 | } catch (ClientException $e) { 240 | $statusCode = $e->getResponse()->getStatusCode(); 241 | $res = json_decode($e->getResponse()->getBody()->getContents()); 242 | 243 | $this->log('Error '.$statusCode, 'GET', $url, null, (array) $res); 244 | 245 | return [$statusCode, $res]; 246 | } 247 | } 248 | 249 | /** 250 | * Get request to SATUSEHAT master data resource 251 | * 252 | * @param [type] $resource 253 | * @param [type] $queryString 254 | * @return void 255 | */ 256 | public function ss_kfa_get($resource, $queryString) 257 | { 258 | 259 | $access_token = $this->token(); 260 | 261 | if (! isset($access_token)) { 262 | return $this->respondError($this->oauth2_error); 263 | } 264 | 265 | $client = new Client; 266 | $headers = [ 267 | 'Authorization' => 'Bearer '.$access_token, 268 | ]; 269 | 270 | $url = $this->base_url.'/kfa-v2/'.$resource.$queryString; 271 | 272 | $request = new Request('GET', $url, $headers); 273 | 274 | try { 275 | $res = $client->sendAsync($request)->wait(); 276 | $statusCode = $res->getStatusCode(); 277 | $response = json_decode($res->getBody()->getContents()); 278 | 279 | if ($resource == 'products/all?') { 280 | if (! empty($response) && empty($response->total)) { 281 | $id = 'Not Found'; 282 | } else { 283 | $id = 'Kfa_GET_'.$resource; 284 | } 285 | } 286 | 287 | if ($resource == 'products?') { 288 | if (! empty($response) && empty($response->result)) { 289 | $id = 'Not Found'; 290 | } else { 291 | $id = 'Kfa_GET_'.$resource; 292 | } 293 | } 294 | 295 | $this->log($id, 'GET', $url, null, (array) $response); 296 | 297 | return [$statusCode, $response]; 298 | } catch (ClientException $e) { 299 | $statusCode = $e->getResponse()->getStatusCode(); 300 | $res = json_decode($e->getResponse()->getBody()->getContents()); 301 | 302 | $this->log('Error '.$statusCode, 'GET', $url, null, (array) $res); 303 | 304 | return [$statusCode, $res]; 305 | } 306 | } 307 | 308 | public function ss_post($resource, $body) 309 | { 310 | $access_token = $this->token(); 311 | 312 | if (! isset($access_token)) { 313 | return $this->respondError($this->oauth2_error); 314 | } 315 | 316 | $client = new Client; 317 | $headers = [ 318 | 'Content-Type' => 'application/json', 319 | 'Authorization' => 'Bearer '.$access_token, 320 | ]; 321 | 322 | $url = $this->fhir_url.($resource == 'Bundle' ? '' : '/'.$resource); 323 | $request = new Request('POST', $url, $headers, $body); 324 | 325 | try { 326 | $res = $client->sendAsync($request)->wait(); 327 | $statusCode = $res->getStatusCode(); 328 | $response = json_decode($res->getBody()->getContents()); 329 | 330 | if ($resource === 'Patient') { 331 | // Patient 332 | 333 | // Get patient identifer 334 | $patient_obj = json_decode($body); 335 | $url = $patient_obj->identifier[0]->system; 336 | $parsed_url = parse_url($url, PHP_URL_PATH); 337 | $exploded_url = explode('/', $parsed_url); 338 | $identifier_type = $exploded_url[2]; 339 | 340 | if ($identifier_type === 'nik') { 341 | if ($response->success !== true) { 342 | $id = 'Error '.$statusCode; 343 | } 344 | $id = $response->data->patient_id; 345 | } elseif ($identifier_type === 'nik-ibu') { 346 | if ($response->create_patient->success !== true) { 347 | $id = 'Error '.$statusCode; 348 | } 349 | $id = $response->create_patient->data->patient_id; 350 | } 351 | } else { 352 | // Other than patient 353 | if ($response->resourceType == 'OperationOutcome' || $statusCode >= 400) { 354 | $id = 'Error '.$statusCode; 355 | } else { 356 | if ($resource == 'Bundle') { 357 | $id = 'Success '.$statusCode; 358 | } else { 359 | $id = $response->id; 360 | } 361 | } 362 | } 363 | $this->log($id, 'POST', $url, (array) json_decode($body), (array) $response); 364 | 365 | return [$statusCode, $response]; 366 | } catch (ClientException $e) { 367 | $statusCode = $e->getResponse()->getStatusCode(); 368 | $res = json_decode($e->getResponse()->getBody()->getContents()); 369 | 370 | $this->log('Error '.$statusCode, 'POST', $url, (array) json_decode($body), (array) $res); 371 | 372 | return [$statusCode, $res]; 373 | } 374 | 375 | $res = $client->sendAsync($request)->wait(); 376 | echo $res->getBody(); 377 | } 378 | 379 | public function ss_put($resource, $id, $body) 380 | { 381 | $access_token = $this->token(); 382 | 383 | if (! isset($access_token)) { 384 | return $this->respondError($this->oauth2_error); 385 | } 386 | 387 | $client = new Client; 388 | $headers = [ 389 | 'Content-Type' => 'application/json', 390 | 'Authorization' => 'Bearer '.$access_token, 391 | ]; 392 | 393 | $url = $this->fhir_url.'/'.$resource.'/'.$id; 394 | $request = new Request('PUT', $url, $headers, $body); 395 | 396 | try { 397 | $res = $client->sendAsync($request)->wait(); 398 | $statusCode = $res->getStatusCode(); 399 | $response = json_decode($res->getBody()->getContents()); 400 | 401 | if ($response->resourceType == 'OperationOutcome' || $statusCode >= 400) { 402 | $id = 'Error '.$statusCode; 403 | } else { 404 | $id = $response->id; 405 | } 406 | $this->log($id, 'PUT', $url, (array) json_decode($body), (array) $response); 407 | 408 | return [$statusCode, $response]; 409 | } catch (ClientException $e) { 410 | $statusCode = $e->getResponse()->getStatusCode(); 411 | $res = json_decode($e->getResponse()->getBody()->getContents()); 412 | 413 | $this->log('Error '.$statusCode, 'PUT', $url, null, (array) $res); 414 | 415 | return [$statusCode, $res]; 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /src/SatusehatIntegrationServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 13 | __DIR__.'/../config/satusehatintegration.php' => config_path('satusehatintegration.php'), 14 | ], 'config'); 15 | 16 | $this->mergeConfigFrom(__DIR__.'/../config/satusehatintegration.php', 'satusehatintegration'); 17 | 18 | // Publish Migrations for Token 19 | if (! class_exists('CreateSatusehatTokenTable')) { 20 | $timestamp = date('Y_m_d_His', time()); 21 | 22 | $this->publishes([ 23 | __DIR__.'/../database/migrations/create_satusehat_token_table.php.stub' => database_path("/migrations/{$timestamp}_create_satusehat_token_table.php"), 24 | ], 'migrations'); 25 | } 26 | 27 | // Publish Migrations for Log 28 | if (! class_exists('CreateSatusehatLogTable')) { 29 | $timestamp = date('Y_m_d_His', time()); 30 | 31 | $this->publishes([ 32 | __DIR__.'/../database/migrations/create_satusehat_log_table.php.stub' => database_path("/migrations/{$timestamp}_create_satusehat_log_table.php"), 33 | ], 'migrations'); 34 | } 35 | 36 | // Publish Migrations for ICD 10 37 | if (! class_exists('CreateSatusehatIcd10Table')) { 38 | $timestamp = date('Y_m_d_His', time()); 39 | 40 | $this->publishes([ 41 | __DIR__.'/../database/migrations/create_satusehat_icd10_table.php.stub' => database_path("/migrations/{$timestamp}_create_satusehat_icd10_table.php"), 42 | ], 'icd10'); 43 | } 44 | 45 | // Publish ICD 10 csv data 46 | $this->publishes([ 47 | __DIR__.'/../database/seeders/csv/icd10.csv.stub' => database_path('/seeders/csv/icd10.csv'), 48 | ], 'icd10'); 49 | 50 | // Publish Seeder for ICD 10 51 | if (! class_exists('Icd10Seeder')) { 52 | $this->publishes([ 53 | __DIR__.'/../database/seeders/Icd10Seeder.php.stub' => database_path('/seeders/Icd10Seeder.php'), 54 | ], 'icd10'); 55 | } 56 | 57 | // Publish Migrations for Kode Wilayah Indonesia 58 | if (! class_exists('CreateKodeWilayahIndonesiaTable')) { 59 | $timestamp = date('Y_m_d_His', time()); 60 | 61 | $this->publishes([ 62 | __DIR__.'/../database/migrations/create_kode_wilayah_indonesia_table.php.stub' => database_path("/migrations/{$timestamp}_create_kode_wilayah_indonesia_table.php"), 63 | ], 'kodewilayahindonesia'); 64 | } 65 | 66 | // Publish Kode Wilayah Indonesia csv data 67 | $this->publishes([ 68 | __DIR__.'/../database/seeders/csv/kode_wilayah_indonesia.csv.stub' => database_path('/seeders/csv/kode_wilayah_indonesia.csv'), 69 | ], 'kodewilayahindonesia'); 70 | 71 | // Publish Seeder for Kode Wilayah Indonesia 72 | if (! class_exists('KodeWilayahIndonesiaSeeder')) { 73 | $this->publishes([ 74 | __DIR__.'/../database/seeders/KodeWilayahIndonesiaSeeder.php.stub' => database_path('/seeders/KodeWilayahIndonesiaSeeder.php'), 75 | ], 'kodewilayahindonesia'); 76 | } 77 | } 78 | 79 | public function register() 80 | { 81 | // 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Terminology/Icd10.php: -------------------------------------------------------------------------------- 1 | connection)) { 26 | $this->setConnection(config('satusehatintegration.database_connection_master')); 27 | } 28 | 29 | if (! isset($this->table)) { 30 | $this->setTable(config('satusehatintegration.icd10_table_name')); 31 | } 32 | 33 | parent::__construct($attributes); 34 | } 35 | 36 | protected $primaryKey = 'id'; 37 | 38 | public $incrementing = false; 39 | 40 | protected $casts = ['icd10_code' => 'string', 'icd10_en' => 'string', 'icd10_id' => 'string']; 41 | } 42 | -------------------------------------------------------------------------------- /src/Terminology/Kfa.php: -------------------------------------------------------------------------------- 1 | identifier)) { 22 | throw new TerminologyInvalidArgumentException('Identifier currently available ('.implode(', ', $this->identifier)."), $identifier given"); 23 | } 24 | 25 | $queryStringBuilder = [ 26 | 'identifier' => $identifier, 27 | 'code' => $code, 28 | ]; 29 | 30 | $queryString = http_build_query($queryStringBuilder); 31 | 32 | return $this->ss_kfa_get('products?', $queryString); 33 | } 34 | 35 | /** 36 | * Get paginated Kfa Products 37 | * 38 | * @param string $productType currently available : 'alkes' | 'farmasi' 39 | * @param int $page min 1 no max 40 | * @param int $size min 1 max 1000 41 | * @return void 42 | */ 43 | public function getProducts(string $productType, ?string $keyword = null, int $page = 1, int $size = 100) 44 | { 45 | if (! in_array($productType, ['alkes', 'farmasi'])) { 46 | throw new TerminologyInvalidArgumentException("\$productType currently available (alkes | farmasi), $productType given."); 47 | } 48 | 49 | if ($size > 1000) { 50 | throw new TerminologyException("Maximum size record 1000/request, $size given."); 51 | } 52 | 53 | if ($page < 1 || $size < 1) { 54 | throw new TerminologyInvalidArgumentException('Page / Size cant be blank.'); 55 | } 56 | 57 | $queryStringBuilder = [ 58 | 'product_type' => $productType, 59 | 'page' => $page, 60 | 'size' => $size, 61 | ]; 62 | 63 | if (! empty($keyword)) { 64 | $queryStringBuilder['keyword'] = $keyword; 65 | } 66 | 67 | $queryString = http_build_query($queryStringBuilder); 68 | 69 | return $this->ss_kfa_get('products/all?', $queryString); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Terminology/KodeWilayahIndonesia.php: -------------------------------------------------------------------------------- 1 | connection)) { 27 | $this->setConnection(config('satusehatintegration.database_connection_master')); 28 | } 29 | 30 | if (! isset($this->table)) { 31 | $this->setTable(config('satusehatintegration.kode_wilayah_indonesia_table_name')); 32 | } 33 | 34 | parent::__construct($attributes); 35 | } 36 | 37 | protected $primaryKey = 'id'; 38 | 39 | public $incrementing = true; // Sesuaikan dengan 'bigIncrements' di migration 40 | 41 | protected $casts = [ 42 | 'kode_wilayah' => 'string', 43 | 'nama_wilayah' => 'string', 44 | 'level' => 'integer', 45 | 'parent' => 'string', 46 | 'state' => 'string', 47 | ]; 48 | } 49 | --------------------------------------------------------------------------------