├── .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 | [](https://packagist.org/packages/ivanwilliammd/satusehat-integration)
4 | [](https://github.com/ivanwilliammd/satusehat-integration/actions/workflows/run-tests.yml)
5 | [](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 |
--------------------------------------------------------------------------------