From b212d39dff92c2ab242f5ded9a93a6cf838ddb99 Mon Sep 17 00:00:00 2001 From: mustofa-id <25121822+mustofa-id@users.noreply.github.com> Date: Sun, 4 Jan 2026 00:28:53 +0700 Subject: [PATCH 1/8] init --- src/ihs.ts | 27 +++- src/kfa.ts | 409 +++++++++++++++++++++++++++++++++++++++++++++++++++++ words.txt | 8 ++ 3 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 src/kfa.ts diff --git a/src/ihs.ts b/src/ihs.ts index 8b91876..04c40af 100644 --- a/src/ihs.ts +++ b/src/ihs.ts @@ -1,9 +1,10 @@ import { getConsentSingleton } from './consent.js'; +import { getKFASingleton } from './kfa.js'; import { getKycSingleton } from './kyc.js'; type MaybePromise = T | Promise; type Mode = 'development' | 'staging' | 'production'; -type API = 'auth' | 'fhir' | 'consent' | 'kyc'; +type API = 'auth' | 'fhir' | 'consent' | 'kyc' | 'kfa' | 'kfa2' | 'kfa3'; type BaseURL = Record>; export interface IHSConfig { @@ -63,19 +64,28 @@ const defaultBaseUrls: BaseURL = { auth: `https://api-satusehat-dev.dto.kemkes.go.id/oauth2/v1`, fhir: `https://api-satusehat-dev.dto.kemkes.go.id/fhir-r4/v1`, consent: `https://api-satusehat-dev.dto.kemkes.go.id/consent/v1`, - kyc: `https://api-satusehat-dev.dto.kemkes.go.id/kyc/v1` + kyc: `https://api-satusehat-dev.dto.kemkes.go.id/kyc/v1`, + kfa: `https://api-satusehat-dev.dto.kemkes.go.id/kfa`, + kfa2: `https://api-satusehat-dev.dto.kemkes.go.id/kfa-v2`, + kfa3: `https://api-satusehat-dev.dto.kemkes.go.id/kfa-v3` }, staging: { auth: `https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1`, fhir: `https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1`, consent: `https://api-satusehat-stg.dto.kemkes.go.id/consent/v1`, - kyc: `https://api-satusehat-stg.dto.kemkes.go.id/kyc/v1` + kyc: `https://api-satusehat-stg.dto.kemkes.go.id/kyc/v1`, + kfa: `https://api-satusehat-stg.dto.kemkes.go.id/kfa`, + kfa2: `https://api-satusehat-stg.dto.kemkes.go.id/kfa-v2`, + kfa3: `https://api-satusehat-stg.dto.kemkes.go.id/kfa-v3` }, production: { auth: `https://api-satusehat.kemkes.go.id/oauth2/v1`, fhir: `https://api-satusehat.kemkes.go.id/fhir-r4/v1`, consent: `https://api-satusehat.kemkes.go.id/consent/v1`, - kyc: `https://api-satusehat.kemkes.go.id/kyc/v1` + kyc: `https://api-satusehat.kemkes.go.id/kyc/v1`, + kfa: `https://api-satusehat.kemkes.go.id/kfa`, + kfa2: `https://api-satusehat.kemkes.go.id/kfa-v2`, + kfa3: `https://api-satusehat.kemkes.go.id/kfa-v3` } } as const; @@ -123,7 +133,7 @@ export default class IHS { } /** - * Request ke API `consent`, `fhir`, dan `kyc` yang dapat diatur + * Request ke API `consent`, `fhir`, `kyc`, dan `kfa` yang dapat diatur * pada property `type` pada parameter `config`. Autentikasi sudah * ditangani secara otomatis pada method ini. */ @@ -143,7 +153,7 @@ export default class IHS { /** * Autentikasi menggunakan `clientSecret` dan `secretKey` dengan kembalian * berupa detail autentikasi termasuk `access_token` yang digunakan untuk - * request ke API `consent`, `fhir`, dan `kyc`. + * request ke API `consent`, `fhir`, `kyc`, dan `kfa`. * * IHS Note: Rate limit is 1 request per minute after a failed attempt. */ @@ -192,6 +202,11 @@ export default class IHS { const instance = getKycSingleton(this); return instance; } + + get kfa() { + const instance = getKFASingleton(this); + return instance; + } } /** diff --git a/src/kfa.ts b/src/kfa.ts new file mode 100644 index 0000000..22020d5 --- /dev/null +++ b/src/kfa.ts @@ -0,0 +1,409 @@ +import IHS from './ihs.js'; + +let instance: KFA | undefined; + +export class KFA { + constructor(private readonly ihs: IHS) {} + + /** + * Mendapatkan harga produk JKN + */ + async getPriceJKN(params: { + /** Isi dengan kode produk KFA yang diinginkan. */ + kfaCode: string; + + /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ + page?: number; + + /** Isi dengan banyaknya data yang ingin ditampilkan dalam satu halaman (page). Default to 50 */ + limit?: number; + + /** + * Isi dengan kode regional JKN yang diinginkan. + * - regional1: Bali, Banten, Jawa Barat, Jawa Timur, Jakarta, Jawa Tengah, Lampung, Yogyakarta + * - regional2: Bangka Belitung, Bengkulu, Jambi, Nusa Tenggara Barat, Riau, Sumatra Barat, Sumatra Selatan, Sumatra Utara + * - regional3: Aceh, Kalimantan Barat, Kalimantan Timur, Kepulauan Riau, Kalimantan Tengah, Sulawesi Utara, Sulawesi Selatan, Sulawesi Tengah + * - regional4: Gorontalo, Kalimantan Tengah, Kalimantan Utara, Sulawesi Tenggara, Sulawesi Barat + * - regional5: Maluku, Maluku Utara, Nusa Tenggara Timur, Papua Barat + * - regional6: Papua Pegunungan, Papua Selatan, Papua Tengah + */ + regionCode?: `regional${1 | 2 | 3 | 4 | 5 | 6}`; + + /** Isi dengan dokumen referensi atau dasar hukum yang berlaku. */ + documentRef?: string; + }) { + try { + const response = await this.ihs.request({ + type: 'kfa', + path: '/farmalkes-price-jkn', + searchParams: { + page: String(params.page || 1), + limit: String(params.limit || 50), + kfa_code: params.kfaCode, + region_code: params.regionCode || '', + document_ref: params.documentRef || '' + } + }); + + if (response.status < 500) { + const json = await response.json(); + json.success = response.ok; + return json; + } + + const text = await response.text(); + throw new Error(text); + } catch (error) { + return asClientError(error); + } + } + + /** + * Mendapatkan detail produk + */ + async getProductDetail(params: { + /** + * Sumber data: + * - `nie` Data Nomor Izin Edar (NIE) yang bersumber dari BPOM. + * - `lkpp` Data inventaris, distribusi, pengelolaan, dan harga obat yang beredar + * bersumber dari Lembaga Kebijakan Pengadaan Barang/Jasa Pemerintah (LKPP). + * - `kfa` Data kode unik produk farmasi dan alat kesehatan yang bersumber pada + * Kamus Farmasi dan Alat Kesehatan (KFA). + */ + identifier: 'nie' | 'lkpp' | 'kfa'; + + /** Isi kode dari produk yang akan dicari. */ + code: string; + }) { + try { + const response = await this.ihs.request({ + type: 'kfa2', + path: '/products', + searchParams: params + }); + + if (response.status < 500) { + const json = await response.json(); + json.success = response.ok; + return json; + } + + const text = await response.text(); + throw new Error(text); + } catch (error) { + return asClientError(error); + } + } + + /** + * Pencarian daftar produk dengan paginasi + */ + async getProducts(params: { + /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ + page?: number; + + /** Isi dengan banyaknya data yang ingin ditampilkan dalam satu halaman (page). Default to 50 */ + size?: number; + + /** Isi dengan kategori/jenis produk yang diinginkan. Contoh: farmasi. */ + productType: string; + + /** Isi dengan waktu mulai dengan format YYYY-MM-DD */ + fromDate?: string; + + /** Isi dengan waktu selesai dengan format YYYY-MM-DD */ + toDate?: string; + + /** Isi dengan kategori/jenis farmalkes yang diinginkan. */ + farmalkesType?: string; + + /** Isi dengan keyword yang diinginkan. Contoh: glove */ + keyword?: string; + + /** Isi dengan kode produk virtual/template (PAV) KFA yang diinginkan. */ + templateCode?: string; + + /** Isi dengan kode kemasan (PAK) KFA yang diinginkan. */ + packagingCode?: string; + }) { + try { + const response = await this.ihs.request({ + type: 'kfa2', + path: '/products/all', + searchParams: { + page: String(params.page || 1), + size: String(params.size || 50), + product_type: params.productType, + from_date: params.fromDate || '', + to_date: params.toDate || '', + farmalkes_type: params.farmalkesType || '', + keyword: params.keyword || '', + template_code: params.templateCode || '', + packaging_code: params.packagingCode || '' + } + }); + + if (response.status < 500) { + const json = await response.json(); + json.success = response.ok; + return json; + } + + const text = await response.text(); + throw new Error(text); + } catch (error) { + return asClientError(error); + } + } + + async getAlkes(params: { + /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ + page?: number; + + /** Isi dengan banyaknya data raw yang ingin ditampilkan dalam satu halaman (page). Default to 50 */ + size?: number; + + /** Isi dengan state varian produk. Terdapat 2 option dalam varian produk `draft` dan `valid`. */ + state: 'draft' | 'valid'; + }) { + try { + const response = await this.ihs.request({ + type: 'kfa3', + path: '/alkes/products', + body: JSON.stringify({ + page: params.page || 1, + size: params.size || 50, + state: params.state + }) + }); + + if (response.status < 500) { + const json = await response.json(); + json.success = response.ok; + return json; + } + + const text = await response.text(); + throw new Error(text); + } catch (error) { + return asClientError(error); + } + } +} + +export function getKFASingleton(...params: ConstructorParameters) { + if (!instance) instance = new KFA(...params); + return instance; +} + +function asClientError(error: unknown): ClientError { + const msg = error instanceof Error ? error.message : JSON.stringify(error); + return { + success: false, + detail: [{ loc: [], msg, type: 'unknown' }] + }; +} + +interface ClientError { + success: false; + detail: { + loc: string[]; + msg: string; + type: string; + }[]; +} + +interface PriceJKN { + success: true; + total: number; + page: number; + limit: number; + items: { + data: { + product_template_name: string; + kfa_code: string; + document_ref: string; + active: boolean; + region_name: string; + region_code: string; + start_date: string; // ISO date string (YYYY-MM-DD) + end_date: string | null; + price_unit: number; + uom_name: string; + updated_at: string; // datetime string (YYYY-MM-DD HH:mm:ss) + uom_pack: string[]; + province: { + province_code: string; + province_name: string; + }[]; + }[]; + }; +} + +interface ProductDetail { + success: true; + search_code: string; + search_identifier: string; + result: { + name: string; + kfa_code: string; + active: boolean; + state: string; + image: string | null; + updated_at: string; + + farmalkes_type: { code: string; name: string; group: string }; + ucum: { cs_code: string; name: string }; + + dosage_form: ProductDetail['result']['farmalkes_type']; + controlled_drug: ProductDetail['result']['farmalkes_type']; + rute_pemberian: ProductDetail['result']['farmalkes_type']; + + uom: { name: string }; + + produksi_buatan: string; + nie: string; + nama_dagang: string; + manufacturer: string; + registrar: string; + generik: boolean; + rxterm: string; + dose_per_unit: number; + fix_price: number; + het_price: number; + farmalkes_hscode: string | null; + tayang_lkpp: boolean; + kode_lkpp: string; + score_tkdn: number | null; + score_bmp: number | null; + score_tkdn_bmp: number | null; + + med_dev_jenis: string | null; + med_dev_subkategori: string | null; + med_dev_kategori: string | null; + med_dev_kelas_risiko: string | null; + klasifikasi_izin: string | null; + + net_weight: number | null; + net_weight_uom_name: string; + volume: number | null; + volume_uom_name: string; + + atc_ddd: { + name: string; + code: string; + level: string; + parent_code: string | boolean; + comment: string | null; + }; + + atc_l1: ProductDetail['result']['atc_ddd']; + atc_l2: ProductDetail['result']['atc_ddd']; + atc_l3: ProductDetail['result']['atc_ddd']; + atc_l4: ProductDetail['result']['atc_ddd']; + atc_l5: ProductDetail['result']['atc_ddd']; + + description: string; + indication: string; + warning: string; + side_effect: string; + + identifier_ids: { + name: string; + code: string; + source_name: string; + url: string | null; + }[]; + + packaging_ids: { + name: string; + kfa_code: string; + pack_price: number; + uom_id: string; + qty: number; + }[]; + + product_template: { + kfa_code: string; + name: string; + state: string; + active: boolean; + display_name: string; + updated_at: string; + }; + + active_ingredients: { + kfa_code: string; + active: boolean; + state: string; + zat_aktif: string; + kekuatan_zat_aktif: string; + updated_at: string; + }[]; + + dosage_usage: string[]; + cvx_info: Record; + + replacement: { + product: string | null; + template: string | null; + }; + + tags: string[]; + }; +} + +interface Product { + success: true; + total: number; + page: number; + size: number; + items: { + data: { + name: string; + kfa_code: string; + active: boolean; + state: string; + image: string | null; + updated_at: string; + farmalkes_type: { code: string; name: string; group: string }; + produksi_buatan: string; + nie: string | null; + nama_dagang: string; + manufacturer: string | null; + registrar: string | null; + generik: boolean; + rxterm: string | null; + dose_per_unit: 1; + fix_price: 40499000.0; + het_price: string | null; + farmalkes_hscode: string | null; + tayang_lkpp: boolean; + kode_lkpp: string | null; + net_weight: string | null; + net_weight_uom_name: string; + volume: string | null; + volume_uom_name: string; + uom: { name: string }; + dosage_form: { code: string | boolean; name: string | boolean }; + product_template: { + kfa_code: string; + name: string; + state: string; + active: boolean; + display_name: string; + updated_at: string; + }; + active_ingredients: string[]; + replacement: { + product: string | null; + template: string | null; + }; + tags: string[]; + }[]; + }; +} + +interface Alkes { + success: true; +} diff --git a/words.txt b/words.txt index b7e120f..d8e7745 100644 --- a/words.txt +++ b/words.txt @@ -1,19 +1,27 @@ accesstoken algoritma +Alkes Autentikasi dekripsi diakses enkripsi +farmalkes fasyankes fhir +hscode Inisialisasi kemkes kriptografi +lkpp OAEP OPTIN OPTOUT PKCS publickey +rxterm Silampari simrs Sriwijaya +subkategori +tkdn +ucum From 1b38492a9e2029a82289f5b433a1fbffe73206e2 Mon Sep 17 00:00:00 2001 From: mustofa-id <25121822+mustofa-id@users.noreply.github.com> Date: Sun, 4 Jan 2026 06:46:37 +0700 Subject: [PATCH 2/8] finalize --- .env.example | 2 +- src/kfa.ts | 281 ++++++++++++++++++++++++++++++++++++++++++++++++--- words.txt | 4 + 3 files changed, 270 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index 164d9e4..18853cc 100644 --- a/.env.example +++ b/.env.example @@ -6,5 +6,5 @@ IHS_KYC_PEM_FILE= TEST_AGENT_NIK= TEST_AGENT_NAME= TEST_PATIENT_NIK= -TEST_PATEINT_NAME= +TEST_PATIENT_NAME= TEST_PATIENT_ID= diff --git a/src/kfa.ts b/src/kfa.ts index 22020d5..e279d2f 100644 --- a/src/kfa.ts +++ b/src/kfa.ts @@ -156,29 +156,39 @@ export class KFA { } } - async getAlkes(params: { - /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ - page?: number; + async getAlkesVariants(params: GetAlkesParams) { + try { + const response = await this.ihs.request({ + type: 'kfa3', + path: '/alkes/products', + method: 'POST', + body: asAlkesRequestBody(params) + }); - /** Isi dengan banyaknya data raw yang ingin ditampilkan dalam satu halaman (page). Default to 50 */ - size?: number; + if (response.status < 500) { + const json = | ClientError>await response.json(); + json.success = response.ok; + return json; + } - /** Isi dengan state varian produk. Terdapat 2 option dalam varian produk `draft` dan `valid`. */ - state: 'draft' | 'valid'; - }) { + const text = await response.text(); + throw new Error(text); + } catch (error) { + return asClientError(error); + } + } + + async getAlkesTemplates(params: GetAlkesParams) { try { const response = await this.ihs.request({ type: 'kfa3', - path: '/alkes/products', - body: JSON.stringify({ - page: params.page || 1, - size: params.size || 50, - state: params.state - }) + path: '/alkes/template', + method: 'POST', + body: asAlkesRequestBody(params) }); if (response.status < 500) { - const json = await response.json(); + const json = | ClientError>await response.json(); json.success = response.ok; return json; } @@ -204,6 +214,25 @@ function asClientError(error: unknown): ClientError { }; } +function asAlkesRequestBody(params: GetAlkesParams) { + return JSON.stringify({ + page: params.page || 1, + size: params.size || 50, + state: params.state, + active: params.active, + kfa_code: params.kfaCode, + reference_code: params.referenceCode, + search: params.search, + updated_from_date: params.updatedFromDate, + updated_to_date: params.updatedToDate, + farmalkes_type: params.farmalkesType, + category_code: params.categoryCode, + sub_category_code: params.subCategoryCode, + type_code: params.typeCode, + sub_type_code: params.subTypeCode + }); +} + interface ClientError { success: false; detail: { @@ -404,6 +433,226 @@ interface Product { }; } -interface Alkes { +interface GetAlkesParams { + /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ + page?: number; + + /** Isi dengan banyaknya data raw yang ingin ditampilkan dalam satu halaman (page). Default to 50 */ + size?: number; + + /** Isi dengan state varian produk. Terdapat 2 option dalam varian produk `draft` dan `valid`. */ + state: 'draft' | 'valid'; + + /** Isi `true` atau `false` menunjukan data sudah terhapus atau belum */ + active: boolean; + + /** Isi dengan kode kfa */ + kfaCode: string; + + /** + * Isi dengan kode dari NIE BPOM/LKPP yang ada pada field identifier_ids[].code. Dapat diisi + * lebih dari satu yang dibatasi dengan 'koma'. Contoh: AKD 21501912107,2649566. + */ + referenceCode: string; + + /** Isi dengan display_name, synonym, atau nama_dagang dalam pencarian fuzzy. Contoh: Surgical Gown. */ + search: string; + + /** Isi dengan tanggal pencarian 'dari' format YYYY-MM-DD. */ + updatedFromDate: string; + + /** Isi dengan tanggal pencarian 'sampai' format YYYY-MM-DD. */ + updatedToDate: string; + + /** Isi dengan tipe kode yang sesuai dengan farmalkes_type.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: `device,pkrt` */ + farmalkesType: string; + + /** Isi dengan kode level 1 yang sesuai dengan kategori.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: 02,03,04. */ + categoryCode: string; + + /** Isi dengan kode level 2 yang sesuai dengan sub_kategori.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: 0204,0205. */ + subCategoryCode: string; + + /** Isi dengan kode level 3 yang sesuai dengan jenis.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: 0204001,0204002. */ + typeCode: string; + + /** Isi dengan kode level 4 yang sesuai dengan jenis.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: 0204001003,0204001005. */ + subTypeCode: string; +} + +interface AlkesResponseBody { success: true; + status: number; + error: boolean; + message: string; + meta: { + item_count: number; + page: { + is_cursor: boolean; + current: number; + previous: number; + next: number; + limit: number; + total: number; + }; + sort: string | null; + param: string | null; + data: T; + }; +} + +interface AlkesVariantData { + kfa_code: string; + active: boolean; + barcode: string; + dapat_dibeli_lkpp: boolean; + discontinued: boolean; + display_name: string; + farmalkes_type: { + code: string; + name: string; + group: string; + }; + fix_price: 81082633; + fornas: boolean; + identifier_ids: { + url: string | null; + code: string; + name: string; + use: string; + end_date: string | null; + start_date: string | null; + source_code: string; + source_name: string; + }[]; + jenis: { + code: string; + name: string; + }; + kategori: { + code: string; + name: string; + }; + klasifikasi_izin: { + code: string; + name: string; + type: string; + }; + kode_kbki: string; + kode_lkpp: string; + manufacturer: string; + manufacturer_country: { + code: string; + name: string; + }; + med_dev_kelas_risiko: string; + nama_dagang: string; + nie: string; + product_template: { + state: string; + kfa_code: string; + name: string; + bmhp: boolean; + synonyms: string; + }; + produksi_buatan: string; + registrar: string; + registrar_country: { + code: string; + name: string; + }; + score_bmp: number; + score_tkdn: number; + score_tkdn_bmp: number; + stok_wajib_yankes: boolean; + sub_jenis: { + code: string; + name: string; + }; + sub_kategori: { + code: string; + name: string; + }; + tayang_lkpp: boolean; + ucum: { + name: string; + ci_code: string; + cs_code: string; + }; + uom_name: string; + uom_po_name: string; + updated_at: string; + variant_desc_farmalkes: string; + variant_desc_usage: string; + variant_desc_warning: string; + variant_side_effect: string; + volume: number; + weight: number; + product_state: string; + replacement: { + product: { + reason: string; + kfa_code: string; + }; + template: { + reason: string; + kfa_code: string; + }; + }; +} + +interface AlkesTemplateData { + kfa_code: string; + active: boolean; + bmhp: boolean; + desc_farmalkes: string; + desc_usage: string; + desc_warning: string; + farmalkes_hscode: string; + farmalkes_type: { + code: string; + name: string; + group: string; + }; + fornas: boolean; + jenis: { + code: string; + name: string; + }; + kategori: { + code: string; + name: string; + }; + klasifikasi_izin: { + code: string; + name: string; + type: string; + }; + med_dev_kelas_risiko: string; + name: string; + replacement: { + name: string; + reason: string; + kfa_code: string; + }; + side_effect: string; + state: string; + stok_wajib_yankes: boolean; + sub_jenis: { + code: string; + name: string; + }; + sub_kategori: { + code: string; + name: string; + }; + synonyms: string; + ucum: { + name: string; + ci_code: string; + cs_code: string; + }; + uom_name: string; + uom_po_name: string; + updated_at: string; } diff --git a/words.txt b/words.txt index d8e7745..57edd41 100644 --- a/words.txt +++ b/words.txt @@ -2,14 +2,17 @@ accesstoken algoritma Alkes Autentikasi +bmhp dekripsi diakses enkripsi farmalkes fasyankes fhir +fornas hscode Inisialisasi +kbki kemkes kriptografi lkpp @@ -17,6 +20,7 @@ OAEP OPTIN OPTOUT PKCS +pkrt publickey rxterm Silampari From 7d377253e2b35707d239a6dae2904713efddf9e7 Mon Sep 17 00:00:00 2001 From: mustofa-id <25121822+mustofa-id@users.noreply.github.com> Date: Sun, 4 Jan 2026 07:09:17 +0700 Subject: [PATCH 3/8] simple test --- src/kfa.spec.ts | 22 ++++++++++++++++++++++ src/kfa.ts | 38 ++++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 src/kfa.spec.ts diff --git a/src/kfa.spec.ts b/src/kfa.spec.ts new file mode 100644 index 0000000..362e092 --- /dev/null +++ b/src/kfa.spec.ts @@ -0,0 +1,22 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import IHS from './ihs.js'; +import { getKFASingleton, KFA } from './kfa.js'; + +let kfa: KFA; + +beforeAll(() => { + const ihs = new IHS(); + kfa = getKFASingleton(ihs); +}); + +describe('kfa', () => { + it('instance should be object and valid', async () => { + expect(kfa).toBeTypeOf('object'); + expect(kfa).toBeInstanceOf(KFA); + }); + + it('get price jkn', async () => { + const result = await kfa.getPriceJKN({ kfaCode: '' }); + expect(result.success).toBe(true); + }); +}); diff --git a/src/kfa.ts b/src/kfa.ts index e279d2f..687d346 100644 --- a/src/kfa.ts +++ b/src/kfa.ts @@ -248,24 +248,26 @@ interface PriceJKN { page: number; limit: number; items: { - data: { - product_template_name: string; - kfa_code: string; - document_ref: string; - active: boolean; - region_name: string; - region_code: string; - start_date: string; // ISO date string (YYYY-MM-DD) - end_date: string | null; - price_unit: number; - uom_name: string; - updated_at: string; // datetime string (YYYY-MM-DD HH:mm:ss) - uom_pack: string[]; - province: { - province_code: string; - province_name: string; - }[]; - }[]; + data: + | { + product_template_name: string; + kfa_code: string; + document_ref: string; + active: boolean; + region_name: string; + region_code: string; + start_date: string; + end_date: string | null; + price_unit: number; + uom_name: string; + updated_at: string; + uom_pack: string[]; + province: { + province_code: string; + province_name: string; + }[]; + }[] + | null; }; } From 62f1bd629d6fd9426d86291262e42ff4bba521b2 Mon Sep 17 00:00:00 2001 From: mustofa-id <25121822+mustofa-id@users.noreply.github.com> Date: Sun, 4 Jan 2026 07:34:52 +0700 Subject: [PATCH 4/8] changeset --- .changeset/fiery-meals-care.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fiery-meals-care.md diff --git a/.changeset/fiery-meals-care.md b/.changeset/fiery-meals-care.md new file mode 100644 index 0000000..95a78a3 --- /dev/null +++ b/.changeset/fiery-meals-care.md @@ -0,0 +1,5 @@ +--- +'@ssecd/ihs': patch +--- + +Implement missing KFA api From e44f6672bc835ded6902ae47202841feb77e03c9 Mon Sep 17 00:00:00 2001 From: mustofa-id <25121822+mustofa-id@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:19:52 +0700 Subject: [PATCH 5/8] fix types --- src/kfa.spec.ts | 2 +- src/kfa.ts | 164 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 111 insertions(+), 55 deletions(-) diff --git a/src/kfa.spec.ts b/src/kfa.spec.ts index 362e092..502901e 100644 --- a/src/kfa.spec.ts +++ b/src/kfa.spec.ts @@ -17,6 +17,6 @@ describe('kfa', () => { it('get price jkn', async () => { const result = await kfa.getPriceJKN({ kfaCode: '' }); - expect(result.success).toBe(true); + expect(result.error).toBe(false); }); }); diff --git a/src/kfa.ts b/src/kfa.ts index 687d346..468c396 100644 --- a/src/kfa.ts +++ b/src/kfa.ts @@ -47,7 +47,7 @@ export class KFA { if (response.status < 500) { const json = await response.json(); - json.success = response.ok; + json.error = !response.ok; return json; } @@ -84,7 +84,7 @@ export class KFA { if (response.status < 500) { const json = await response.json(); - json.success = response.ok; + json.error = !response.ok; return json; } @@ -145,7 +145,7 @@ export class KFA { if (response.status < 500) { const json = await response.json(); - json.success = response.ok; + json.error = !response.ok; return json; } @@ -162,19 +162,20 @@ export class KFA { type: 'kfa3', path: '/alkes/products', method: 'POST', + headers: { 'content-type': 'application/json' }, body: asAlkesRequestBody(params) }); if (response.status < 500) { - const json = | ClientError>await response.json(); - json.success = response.ok; + const json = >await response.json(); + json.error = !response.ok; return json; } const text = await response.text(); throw new Error(text); } catch (error) { - return asClientError(error); + return asAlkesError(error); } } @@ -184,19 +185,20 @@ export class KFA { type: 'kfa3', path: '/alkes/template', method: 'POST', + headers: { 'content-type': 'application/json' }, body: asAlkesRequestBody(params) }); if (response.status < 500) { - const json = | ClientError>await response.json(); - json.success = response.ok; + const json = >await response.json(); + json.error = !response.ok; return json; } const text = await response.text(); throw new Error(text); } catch (error) { - return asClientError(error); + return asAlkesError(error); } } } @@ -209,11 +211,21 @@ export function getKFASingleton(...params: ConstructorParameters) { function asClientError(error: unknown): ClientError { const msg = error instanceof Error ? error.message : JSON.stringify(error); return { - success: false, + error: true, detail: [{ loc: [], msg, type: 'unknown' }] }; } +function asAlkesError(error: unknown): AlkesResponseBody { + const msg = error instanceof Error ? error.message : JSON.stringify(error); + return { + status: 500, + error: true, + message: msg, + data: null + }; +} + function asAlkesRequestBody(params: GetAlkesParams) { return JSON.stringify({ page: params.page || 1, @@ -234,7 +246,7 @@ function asAlkesRequestBody(params: GetAlkesParams) { } interface ClientError { - success: false; + error: true; detail: { loc: string[]; msg: string; @@ -243,7 +255,7 @@ interface ClientError { } interface PriceJKN { - success: true; + error: false; total: number; page: number; limit: number; @@ -272,7 +284,7 @@ interface PriceJKN { } interface ProductDetail { - success: true; + error: false; search_code: string; search_identifier: string; result: { @@ -285,26 +297,25 @@ interface ProductDetail { farmalkes_type: { code: string; name: string; group: string }; ucum: { cs_code: string; name: string }; + uom: { cs_code?: string; name: string }; dosage_form: ProductDetail['result']['farmalkes_type']; controlled_drug: ProductDetail['result']['farmalkes_type']; rute_pemberian: ProductDetail['result']['farmalkes_type']; - uom: { name: string }; - produksi_buatan: string; nie: string; nama_dagang: string; manufacturer: string; registrar: string; - generik: boolean; - rxterm: string; + generik: boolean | null; + rxterm: number; dose_per_unit: number; fix_price: number; het_price: number; farmalkes_hscode: string | null; - tayang_lkpp: boolean; - kode_lkpp: string; + tayang_lkpp: boolean | null; + kode_lkpp: string | null; score_tkdn: number | null; score_bmp: number | null; score_tkdn_bmp: number | null; @@ -324,7 +335,7 @@ interface ProductDetail { name: string; code: string; level: string; - parent_code: string | boolean; + parent_code: string | null; comment: string | null; }; @@ -372,20 +383,40 @@ interface ProductDetail { updated_at: string; }[]; - dosage_usage: string[]; + dosage_usage: { + qty: number; + name: string; + period: number; + qty_uom: string; + category: string; + duration: number; + qty_high: number; + qty_ucum: number | null; + use_ucum: boolean; + frequency: number; + updated_at: string; + period_ucum: string; + display_name: string; + duration_max: number; + duration_ucum: string; + frequency_max: number; + body_weight_max: number; + body_weight_min: number; + }[]; + cvx_info: Record; replacement: { - product: string | null; - template: string | null; + product: { name: string | null; reason: string | null; kfa_code: string | null } | null; + template: { name: string | null; reason: string | null; kfa_code: string | null } | null; }; - tags: string[]; + tags: { code: string | null; name: string | null }[]; }; } interface Product { - success: true; + error: false; total: number; page: number; size: number; @@ -403,34 +434,50 @@ interface Product { nama_dagang: string; manufacturer: string | null; registrar: string | null; - generik: boolean; - rxterm: string | null; + generik: boolean | null; + rxterm: number | null; dose_per_unit: 1; - fix_price: 40499000.0; - het_price: string | null; + fix_price: number; + het_price: number | null; farmalkes_hscode: string | null; tayang_lkpp: boolean; kode_lkpp: string | null; - net_weight: string | null; + net_weight: number | null; net_weight_uom_name: string; volume: string | null; volume_uom_name: string; uom: { name: string }; - dosage_form: { code: string | boolean; name: string | boolean }; + dosage_form: { code: string | null; name: string | null }; product_template: { kfa_code: string; name: string; state: string; + bmhp: boolean | null; active: boolean; display_name: string; updated_at: string; }; - active_ingredients: string[]; + active_ingredients: { + state: string; + active: boolean; + kfa_code: string; + zat_aktif: string; + updated_at: string; + kekuatan_zat_aktif: string; + }[]; replacement: { - product: string | null; - template: string | null; + product: { name: string | null; reason: string | null; kfa_code: string | null } | null; + template: { name: string | null; reason: string | null; kfa_code: string | null } | null; }; - tags: string[]; + tags: { code: string | null; name: string | null }[]; + paket_obat: { + kfa_code: string | null; + qty: number; + uom_name: string | null; + ucum_cs_code: string | null; + ucum_name: string | null; + }[]; + total_data: number; }[]; }; } @@ -482,26 +529,29 @@ interface GetAlkesParams { subTypeCode: string; } -interface AlkesResponseBody { - success: true; +type AlkesResponseBody = { status: number; - error: boolean; message: string; - meta: { - item_count: number; - page: { - is_cursor: boolean; - current: number; - previous: number; - next: number; - limit: number; - total: number; - }; - sort: string | null; - param: string | null; - data: T; - }; -} + data: T | null; +} & ( + | { error: true } + | { + error: false; + meta: { + item_count: number; + page: { + is_cursor: boolean; + current: number; + previous: number; + next: number; + limit: number; + total: number; + }; + sort: string | null; + param: string | null; + }; + } +); interface AlkesVariantData { kfa_code: string; @@ -515,7 +565,7 @@ interface AlkesVariantData { name: string; group: string; }; - fix_price: 81082633; + fix_price: number; fornas: boolean; identifier_ids: { url: string | null; @@ -601,6 +651,12 @@ interface AlkesVariantData { kfa_code: string; }; }; + lkpp_status: { + freezed: boolean; + reason_code: string | null; + reason: string | null; + remark: string | null; + }; } interface AlkesTemplateData { From 3648a0ebd2ca1bf0877f8a335ee79b132b8a378c Mon Sep 17 00:00:00 2001 From: mustofa-id <25121822+mustofa-id@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:48:18 +0700 Subject: [PATCH 6/8] doc --- README.md | 19 ++++++++++++++++--- src/kfa.ts | 17 ++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e2b9946..4520cb1 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ ihs.authStore = new RedisAuthStore(); export default ihs; ``` -### Patient Consent API +### Patient Consent Pada Patient Consent, terdapat dua buah method yang di-definisikan sesuai dengan spesifikasi IHS yakni method untuk mendapatkan informasi consent pasien: @@ -142,7 +142,7 @@ if (result.resourceType === 'Consent') { Setiap method pada Patient Consent API ini memiliki nilai kembalian FHIR resource `Consent` jika request sukses dan `OperationOutcome` jika request gagal. -### FHIR API +### FHIR Pada API ini, implementasi-nya sangat sederhana dan mengutamakan fleksibilitas yakni dengan hanya mengembalikan `Response` object sehingga response sepenuhnya di-_handle_ pengguna. @@ -160,7 +160,7 @@ if (response.ok) { } ``` -### KYC API +### KYC Pada API ini, terdapat dua buah method yakni method untuk melakukan proses Generate URL Validasi di mana URL digunakan untuk melakukan verifikasi akun SatuSehat melalui SatuSehat Mobile: @@ -218,6 +218,19 @@ Proses kriptografi pada API ini memerlukan file _server key_ atau _public key_ d File _public key_ atau _server key_ dapat di-unduh di [sini](https://github.com/ssecd/ihs/issues/2). +### KFA + +API KFA atau Kamus Farmasi & Alat Kesehatan ini diimplementasikan sesuai dengan spesifikasi IHS pada Playbook yang mana memiliki beberapa versi yakni `kfa`, `kfa-v2`, dan `kfa-v3`. Semua RestAPI yang tersedia sudah dibungkus pada method sehingga mempermudah pemanggilan API dan penulisan tipe request dan response. + +```ts +const result = await ihs.kfa.getProducts({ ... }); +if (result.error) { + // result == ClientError +} else { + // result == Product +} +``` + ## Konfigurasi Konfigurasi mengikuti interface berikut: diff --git a/src/kfa.ts b/src/kfa.ts index 468c396..3e4f719 100644 --- a/src/kfa.ts +++ b/src/kfa.ts @@ -6,7 +6,8 @@ export class KFA { constructor(private readonly ihs: IHS) {} /** - * Mendapatkan harga produk JKN + * Mendapatkan harga produk JKN. + * Menggunakan RestAPI KFA versi pertama. */ async getPriceJKN(params: { /** Isi dengan kode produk KFA yang diinginkan. */ @@ -59,7 +60,8 @@ export class KFA { } /** - * Mendapatkan detail produk + * Mendapatkan detail produk. + * Menggunakan RestAPI KFA versi 2. */ async getProductDetail(params: { /** @@ -96,7 +98,8 @@ export class KFA { } /** - * Pencarian daftar produk dengan paginasi + * Pencarian daftar produk dengan paginasi. + * Menggunakan RestAPI KFA versi 2. */ async getProducts(params: { /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ @@ -156,6 +159,10 @@ export class KFA { } } + /** + * Mendapatkan varian alat kesehatan. + * Menggunakan RestAPI KFA versi 3. + */ async getAlkesVariants(params: GetAlkesParams) { try { const response = await this.ihs.request({ @@ -179,6 +186,10 @@ export class KFA { } } + /** + * Mendapatkan template alat kesehatan. + * Menggunakan RestAPI KFA versi 3. + */ async getAlkesTemplates(params: GetAlkesParams) { try { const response = await this.ihs.request({ From 714837561b3e15483ee1cbea428167b5931fe6e5 Mon Sep 17 00:00:00 2001 From: mustofa-id <25121822+mustofa-id@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:54:21 +0700 Subject: [PATCH 7/8] update doc --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4520cb1..a4b01f0 100644 --- a/README.md +++ b/README.md @@ -223,12 +223,19 @@ File _public key_ atau _server key_ dapat di-unduh di [sini](https://github.com/ API KFA atau Kamus Farmasi & Alat Kesehatan ini diimplementasikan sesuai dengan spesifikasi IHS pada Playbook yang mana memiliki beberapa versi yakni `kfa`, `kfa-v2`, dan `kfa-v3`. Semua RestAPI yang tersedia sudah dibungkus pada method sehingga mempermudah pemanggilan API dan penulisan tipe request dan response. ```ts -const result = await ihs.kfa.getProducts({ ... }); -if (result.error) { +const result1 = await ihs.kfa.getProducts({ ... }); +if (result1.error) { // result == ClientError } else { // result == Product } + +const result2 = await ihs.kfa.getAlkesVariants({ ... }); +if (result2.error) { + // result == AlkesResponseBody +} else { + // result == AlkesResponseBody +} ``` ## Konfigurasi From 5c42ed033558b3cc7a28d0f2a4d6be209c7fa9ac Mon Sep 17 00:00:00 2001 From: mustofa-id <25121822+mustofa-id@users.noreply.github.com> Date: Sun, 4 Jan 2026 18:09:45 +0700 Subject: [PATCH 8/8] improve kfa2 types --- src/kfa.ts | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/kfa.ts b/src/kfa.ts index 3e4f719..e8a8ac4 100644 --- a/src/kfa.ts +++ b/src/kfa.ts @@ -16,7 +16,7 @@ export class KFA { /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ page?: number; - /** Isi dengan banyaknya data yang ingin ditampilkan dalam satu halaman (page). Default to 50 */ + /** Isi dengan banyaknya data yang ingin ditampilkan dalam satu halaman (page). Default to 10 */ limit?: number; /** @@ -39,7 +39,7 @@ export class KFA { path: '/farmalkes-price-jkn', searchParams: { page: String(params.page || 1), - limit: String(params.limit || 50), + limit: String(params.limit || 10), kfa_code: params.kfaCode, region_code: params.regionCode || '', document_ref: params.documentRef || '' @@ -105,7 +105,7 @@ export class KFA { /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ page?: number; - /** Isi dengan banyaknya data yang ingin ditampilkan dalam satu halaman (page). Default to 50 */ + /** Isi dengan banyaknya data yang ingin ditampilkan dalam satu halaman (page). Default to 10 */ size?: number; /** Isi dengan kategori/jenis produk yang diinginkan. Contoh: farmasi. */ @@ -135,7 +135,7 @@ export class KFA { path: '/products/all', searchParams: { page: String(params.page || 1), - size: String(params.size || 50), + size: String(params.size || 10), product_type: params.productType, from_date: params.fromDate || '', to_date: params.toDate || '', @@ -163,7 +163,7 @@ export class KFA { * Mendapatkan varian alat kesehatan. * Menggunakan RestAPI KFA versi 3. */ - async getAlkesVariants(params: GetAlkesParams) { + async getAlkesVariants(params?: GetAlkesParams) { try { const response = await this.ihs.request({ type: 'kfa3', @@ -190,7 +190,7 @@ export class KFA { * Mendapatkan template alat kesehatan. * Menggunakan RestAPI KFA versi 3. */ - async getAlkesTemplates(params: GetAlkesParams) { + async getAlkesTemplates(params?: GetAlkesParams) { try { const response = await this.ihs.request({ type: 'kfa3', @@ -237,10 +237,12 @@ function asAlkesError(error: unknown): AlkesResponseBody { }; } -function asAlkesRequestBody(params: GetAlkesParams) { +function asAlkesRequestBody(params?: GetAlkesParams) { + const defaultParams = { page: 1, size: 10 }; + if (!params) return JSON.stringify(defaultParams); return JSON.stringify({ - page: params.page || 1, - size: params.size || 50, + page: params.page || defaultParams.page, + size: params.size || defaultParams.size, state: params.state, active: params.active, kfa_code: params.kfaCode, @@ -497,47 +499,47 @@ interface GetAlkesParams { /** Isi dengan nomor halaman (page) yang diinginkan. Default to 1 */ page?: number; - /** Isi dengan banyaknya data raw yang ingin ditampilkan dalam satu halaman (page). Default to 50 */ + /** Isi dengan banyaknya data raw yang ingin ditampilkan dalam satu halaman (page). Default to 10 */ size?: number; /** Isi dengan state varian produk. Terdapat 2 option dalam varian produk `draft` dan `valid`. */ - state: 'draft' | 'valid'; + state?: 'draft' | 'valid'; /** Isi `true` atau `false` menunjukan data sudah terhapus atau belum */ - active: boolean; + active?: boolean; /** Isi dengan kode kfa */ - kfaCode: string; + kfaCode?: string; /** * Isi dengan kode dari NIE BPOM/LKPP yang ada pada field identifier_ids[].code. Dapat diisi * lebih dari satu yang dibatasi dengan 'koma'. Contoh: AKD 21501912107,2649566. */ - referenceCode: string; + referenceCode?: string; /** Isi dengan display_name, synonym, atau nama_dagang dalam pencarian fuzzy. Contoh: Surgical Gown. */ - search: string; + search?: string; /** Isi dengan tanggal pencarian 'dari' format YYYY-MM-DD. */ - updatedFromDate: string; + updatedFromDate?: string; /** Isi dengan tanggal pencarian 'sampai' format YYYY-MM-DD. */ - updatedToDate: string; + updatedToDate?: string; /** Isi dengan tipe kode yang sesuai dengan farmalkes_type.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: `device,pkrt` */ - farmalkesType: string; + farmalkesType?: string; /** Isi dengan kode level 1 yang sesuai dengan kategori.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: 02,03,04. */ - categoryCode: string; + categoryCode?: string; /** Isi dengan kode level 2 yang sesuai dengan sub_kategori.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: 0204,0205. */ - subCategoryCode: string; + subCategoryCode?: string; /** Isi dengan kode level 3 yang sesuai dengan jenis.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: 0204001,0204002. */ - typeCode: string; + typeCode?: string; /** Isi dengan kode level 4 yang sesuai dengan jenis.code. Dapat diisi lebih dari satu yang dibatasi dengan 'koma'. Contoh: 0204001003,0204001005. */ - subTypeCode: string; + subTypeCode?: string; } type AlkesResponseBody = {