From e2315a1df54f9f500dbe0e1c9d09b6836b5bd683 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Sat, 14 Feb 2026 16:46:58 +0800 Subject: [PATCH 01/29] feat: add type pcare for diagnosa --- src/fetcher.ts | 850 ++++++++++++++++++++++-------------------- src/pcare/diagnosa.ts | 17 + src/pcare/index.ts | 21 +- 3 files changed, 473 insertions(+), 415 deletions(-) create mode 100644 src/pcare/diagnosa.ts diff --git a/src/fetcher.ts b/src/fetcher.ts index 1f6d416..13dfab7 100644 --- a/src/fetcher.ts +++ b/src/fetcher.ts @@ -1,402 +1,439 @@ -import lz from 'lz-string'; -import { createDecipheriv, createHash, createHmac } from 'node:crypto'; +import lz from "lz-string"; +import { createDecipheriv, createHash, createHmac } from "node:crypto"; type MaybePromise = T | Promise; -export type Mode = 'development' | 'production'; +export type Mode = "development" | "production"; -export type Type = 'aplicares' | 'vclaim' | 'antrean' | 'apotek' | 'pcare' | 'icare' | 'rekamMedis'; +export type Type = + | "aplicares" + | "vclaim" + | "antrean" + | "apotek" + | "pcare" + | "icare" + | "rekamMedis"; export interface Config { - /** - * Kode PPK yang diberikan BPJS. - * - * Diperlukan untuk melakukan proses enkripsi - * pada service eRekamMedis and request pada - * service Aplicares - * - * @default process.env.JKN_PPK_CODE - */ - ppkCode: string; - - /** - * Cons ID dari BPJS - * - * @default process.env.JKN_CONS_ID - */ - consId: string; - - /** - * Secret key dari BPJS - * - * @default process.env.JKN_CONS_SECRET - */ - consSecret: string; - - /** - * User key Aplicares dari BPJS atau gunakan user key vclaim - * - * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_APLICARES_USER_KEY - */ - aplicaresUserKey?: string; - - /** - * User key VClaim dari BPJS - * - * @default process.env.JKN_VCLAIM_USER_KEY - */ - vclaimUserKey: string; - - /** - * User key Antrean dari BPJS - * - * @default process.env.JKN_ANTREAN_USER_KEY - */ - antreanUserKey: string; - - /** - * User key Apotek dari BPJS atau gunakan user key vclaim - * - * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_APOTEK_USER_KEY - */ - apotekUserKey?: string; - - /** - * User key PCare dari BPJS - * - * @default process.env.JKN_PCARE_USER_KEY - */ - pcareUserKey: string; - - /** - * User key i-Care dari BPJS - * - * Umumnya user key i-Care ini nilai sama dengan user key VClaim - * untuk FKRTL dan PCare untuk FKTP - * - * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_ICARE_USER_KEY - */ - icareUserKey?: string; - - /** - * User key eRekam Medis dari BPJS atau gunakan user key vclaim - * - * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_REKAM_MEDIS_USER_KEY - */ - rekamMedisUserKey?: string; - - /** - * Berupa mode "development" dan "production". Secara default akan - * membaca nilai environment variable NODE_ENV atau "development" - * jika NODE_ENV tidak terdapat nilai. Mode ini berpengaruh pada - * nilai konfigurasi yang digunakan dan JKN API base url. - * - * @default process.env.NODE_ENV || "development" - */ - mode: Mode; - - /** - * Secara default bernilai `false` sehingga setiap terjadi kesalahan - * saat mengirim permintaan ke server JKN menggunakan method `send()`, - * pesan kesalahan akan dikembalikan sebagai pesan response dan log - * error akan dicetak pada konsol atau terminal. Jika bernilai true, - * maka kesalahan akan di-throw. - * - * @default false - */ - throw: boolean; - - /** - * Base URL web service dari BPJS. Secara default sudah diatur - * berdasarkan base url yang ada di TrustMark. Nilai dapat diatur - * secara partial, misalnya: - * - * ``` - * baseUrls: { - * vclaim: { - * development: 'http://dev.example.com', - * production: 'http://prod.example.com' - * } - * } - * ``` - */ - baseUrls: Partial>>; + /** + * Kode PPK yang diberikan BPJS. + * + * Diperlukan untuk melakukan proses enkripsi + * pada service eRekamMedis and request pada + * service Aplicares + * + * @default process.env.JKN_PPK_CODE + */ + ppkCode: string; + + /** + * Cons ID dari BPJS + * + * @default process.env.JKN_CONS_ID + */ + consId: string; + + /** + * Secret key dari BPJS + * + * @default process.env.JKN_CONS_SECRET + */ + consSecret: string; + + /** + * User key Aplicares dari BPJS atau gunakan user key vclaim + * + * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_APLICARES_USER_KEY + */ + aplicaresUserKey?: string; + + /** + * User key VClaim dari BPJS + * + * @default process.env.JKN_VCLAIM_USER_KEY + */ + vclaimUserKey: string; + + /** + * User key Antrean dari BPJS + * + * @default process.env.JKN_ANTREAN_USER_KEY + */ + antreanUserKey: string; + + /** + * User key Apotek dari BPJS atau gunakan user key vclaim + * + * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_APOTEK_USER_KEY + */ + apotekUserKey?: string; + + /** + * User key PCare dari BPJS + * + * @default process.env.JKN_PCARE_USER_KEY + */ + pcareUserKey: string; + + /** + * User dan Password PCare dari BPJS + * contoh: username:password + * + * @default process.env.JKN_PCARE_USER_PASS + */ + pcareUserPass: string; + + /** + * User key i-Care dari BPJS + * + * Umumnya user key i-Care ini nilai sama dengan user key VClaim + * untuk FKRTL dan PCare untuk FKTP + * + * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_ICARE_USER_KEY + */ + icareUserKey?: string; + + /** + * User key eRekam Medis dari BPJS atau gunakan user key vclaim + * + * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_REKAM_MEDIS_USER_KEY + */ + rekamMedisUserKey?: string; + + /** + * Berupa mode "development" dan "production". Secara default akan + * membaca nilai environment variable NODE_ENV atau "development" + * jika NODE_ENV tidak terdapat nilai. Mode ini berpengaruh pada + * nilai konfigurasi yang digunakan dan JKN API base url. + * + * @default process.env.NODE_ENV || "development" + */ + mode: Mode; + + /** + * Secara default bernilai `false` sehingga setiap terjadi kesalahan + * saat mengirim permintaan ke server JKN menggunakan method `send()`, + * pesan kesalahan akan dikembalikan sebagai pesan response dan log + * error akan dicetak pada konsol atau terminal. Jika bernilai true, + * maka kesalahan akan di-throw. + * + * @default false + */ + throw: boolean; + + /** + * Base URL web service dari BPJS. Secara default sudah diatur + * berdasarkan base url yang ada di TrustMark. Nilai dapat diatur + * secara partial, misalnya: + * + * ``` + * baseUrls: { + * vclaim: { + * development: 'http://dev.example.com', + * production: 'http://prod.example.com' + * } + * } + * ``` + */ + baseUrls: Partial>>; } export interface SendOption { - /** name of request, it helpful for log or collect stats */ - name?: string; - path: `/${string}` | [`/${string}`, Record]; - method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; - data?: unknown; - skipDecrypt?: boolean; - headers?: Record; - - /** - * Mengatur "Content-Type" pada request header menjadi "application/json; charset=utf-8". - * Secara default "Content-Type" pada request header adalah "application/x-www-form-urlencoded" - * meski pun saat request nilai body dalam format json. Ini tidak standar, namun mengacu pada - * panduan bridging pada halaman dokumentasi di situs TrustMark yang memaksa hal tersebut. - * - * @default undefined - */ - skipContentTypeHack?: boolean; + /** name of request, it helpful for log or collect stats */ + name?: string; + path: + | `/${string}` + | [`/${string}`, Record]; + method?: "GET" | "POST" | "PUT" | "DELETE"; + data?: unknown; + skipDecrypt?: boolean; + headers?: Record; + + /** + * Mengatur "Content-Type" pada request header menjadi "application/json; charset=utf-8". + * Secara default "Content-Type" pada request header adalah "application/x-www-form-urlencoded" + * meski pun saat request nilai body dalam format json. Ini tidak standar, namun mengacu pada + * panduan bridging pada halaman dokumentasi di situs TrustMark yang memaksa hal tersebut. + * + * @default undefined + */ + skipContentTypeHack?: boolean; } export interface LowerResponse { - response: T; - metadata: { - code: C; - message: string; - } & E; + response: T; + metadata: { + code: C; + message: string; + } & E; } export interface CamelResponse { - response: T; - metaData: { - code: C; - message: string; - } & E; + response: T; + metaData: { + code: C; + message: string; + } & E; } export type SendResponse = { - aplicares: LowerResponse; - antrean: LowerResponse; - vclaim: CamelResponse; - apotek: CamelResponse; - pcare: CamelResponse; - icare: CamelResponse; - rekamMedis: LowerResponse; + aplicares: LowerResponse; + antrean: LowerResponse; + vclaim: CamelResponse; + apotek: CamelResponse; + pcare: CamelResponse; + icare: CamelResponse; + rekamMedis: LowerResponse; }; const defaultBaseUrls: Record> = { - aplicares: { - development: 'https://dvlp.bpjs-kesehatan.go.id:8888/aplicaresws', - production: 'https://new-api.bpjs-kesehatan.go.id/aplicaresws' - }, - vclaim: { - development: 'https://apijkn-dev.bpjs-kesehatan.go.id/vclaim-rest-dev', - production: 'https://apijkn.bpjs-kesehatan.go.id/vclaim-rest' - }, - antrean: { - development: 'https://apijkn-dev.bpjs-kesehatan.go.id/antreanrs_dev', - production: 'https://apijkn.bpjs-kesehatan.go.id/antreanrs' - }, - apotek: { - development: 'https://apijkn-dev.bpjs-kesehatan.go.id/apotek-rest-dev', - production: 'https://apijkn.bpjs-kesehatan.go.id/apotek-rest' - }, - pcare: { - development: 'https://apijkn-dev.bpjs-kesehatan.go.id/pcare-rest-dev', - production: 'https://apijkn.bpjs-kesehatan.go.id/pcare-rest' - }, - icare: { - development: 'https://apijkn-dev.bpjs-kesehatan.go.id/ihs_dev', - production: 'https://apijkn.bpjs-kesehatan.go.id/wsihs' - }, - rekamMedis: { - development: 'https://apijkn-dev.bpjs-kesehatan.go.id/erekammedis_dev', - production: 'https://apijkn.bpjs-kesehatan.go.id/medicalrecord' - } + aplicares: { + development: "https://dvlp.bpjs-kesehatan.go.id:8888/aplicaresws", + production: "https://new-api.bpjs-kesehatan.go.id/aplicaresws", + }, + vclaim: { + development: "https://apijkn-dev.bpjs-kesehatan.go.id/vclaim-rest-dev", + production: "https://apijkn.bpjs-kesehatan.go.id/vclaim-rest", + }, + antrean: { + development: "https://apijkn-dev.bpjs-kesehatan.go.id/antreanrs_dev", + production: "https://apijkn.bpjs-kesehatan.go.id/antreanrs", + }, + apotek: { + development: "https://apijkn-dev.bpjs-kesehatan.go.id/apotek-rest-dev", + production: "https://apijkn.bpjs-kesehatan.go.id/apotek-rest", + }, + pcare: { + development: "https://apijkn-dev.bpjs-kesehatan.go.id/pcare-rest-dev", + production: "https://apijkn.bpjs-kesehatan.go.id/pcare-rest", + }, + icare: { + development: "https://apijkn-dev.bpjs-kesehatan.go.id/ihs_dev", + production: "https://apijkn.bpjs-kesehatan.go.id/wsihs", + }, + rekamMedis: { + development: "https://apijkn-dev.bpjs-kesehatan.go.id/erekammedis_dev", + production: "https://apijkn.bpjs-kesehatan.go.id/medicalrecord", + }, }; export class Fetcher { - // simply using custom event function instead of node:EventEmitter - public onRequest: ((info: SendOption & { type: Type }) => MaybePromise) | undefined = - undefined; - - public onResponse: - | (( - info: SendOption & { - /** in milliseconds */ duration: number; - type: T; - }, - result: SendResponse[T] - ) => MaybePromise) - | undefined = undefined; - - public onError: ((error: Error) => MaybePromise) | undefined = undefined; - - private configured = false; - - private config: Config = { - mode: process.env.NODE_ENV !== 'production' ? 'development' : process.env.NODE_ENV, - ppkCode: process.env.JKN_PPK_CODE ?? '', - consId: process.env.JKN_CONS_ID ?? '', - consSecret: process.env.JKN_CONS_SECRET ?? '', - aplicaresUserKey: process.env.JKN_APLICARES_USER_KEY, - vclaimUserKey: process.env.JKN_VCLAIM_USER_KEY ?? '', - antreanUserKey: process.env.JKN_ANTREAN_USER_KEY ?? '', - apotekUserKey: process.env.JKN_APOTEK_USER_KEY, - pcareUserKey: process.env.JKN_PCARE_USER_KEY ?? '', - icareUserKey: process.env.JKN_ICARE_USER_KEY, - rekamMedisUserKey: process.env.JKN_REKAM_MEDIS_USER_KEY, - throw: false, - baseUrls: defaultBaseUrls - }; - - constructor(private userConfig?: Partial | (() => MaybePromise>)) {} - - private async applyConfig() { - if (this.configured) return; - - if (typeof this.userConfig === 'object') { - this.config = this.mergeConfig(this.config, this.userConfig); - } else if (typeof this.userConfig === 'function') { - const userConfig = await this.userConfig(); - this.config = this.mergeConfig(this.config, userConfig); - } - - if (!this.config.consId || !this.config.consSecret) { - throw new Error(`cons id and secret are not defined`); - } - - // fallback to vclaimUserKey - for (const key of [ - 'aplicaresUserKey', - 'apotekUserKey', - 'icareUserKey', - 'rekamMedisUserKey' - ] satisfies (keyof Config)[]) { - if (this.config[key]) continue; - this.config[key] = this.config.vclaimUserKey; - } - - this.configured = true; - } - - private mergeConfig(target: Config, source: Partial): Config { - // simple object merge strategy because only baseUrls is typeof object for now - const baseUrls = { ...target.baseUrls, ...source.baseUrls }; - return { ...target, ...source, baseUrls }; - } - - private get userKeyMap(): Record { - return { - aplicares: this.config.aplicaresUserKey, - vclaim: this.config.vclaimUserKey, - antrean: this.config.antreanUserKey, - apotek: this.config.apotekUserKey, - pcare: this.config.pcareUserKey, - icare: this.config.icareUserKey, - rekamMedis: this.config.rekamMedisUserKey - }; - } - - private getDefaultHeaders(type: Type) { - const userKey = this.userKeyMap[type]; - if (!userKey) throw new Error(`failed to get user key of type "${type}"`); - - const { consId, consSecret } = this.config; - const timestamp = Math.round(Date.now() / 1000); - const message = `${consId}&${timestamp}`; - const signature = createHmac('SHA256', consSecret).update(message).digest('base64'); - return { - 'X-cons-id': consId, - 'X-timestamp': String(timestamp), - 'X-signature': encodeURI(signature), - user_key: userKey - }; - } - - private decrypt(responseText: string, requestTimestamp: string) { - const { consId, consSecret } = this.config; - const keyPlain = `${consId}${consSecret}${requestTimestamp}`; - const key = createHash('sha256').update(keyPlain, 'utf8').digest(); - const iv = Uint8Array.from(key.subarray(0, 16)); - const decipher = createDecipheriv('aes-256-cbc', key, iv); - let dec = decipher.update(responseText, 'base64', 'utf8'); - dec += decipher.final('utf8'); - return dec; - } - - private decompress(text: string): string { - return lz.decompressFromEncodedURIComponent(text); - } - - async send( - type: T, - option: SendOption - ): Promise[T]> { - await this.applyConfig(); - - const path = normalizePath(option.path); - const baseUrl = this.config.baseUrls[type]; - if (!baseUrl) throw new Error(`Invalid base URL for type "${type}"`); - - let result = ''; - try { - const url = new URL(baseUrl[this.config.mode] + path); - const init: RequestInit = { method: option.method ?? 'GET' }; - const headers = { ...this.getDefaultHeaders(type), ...(option.headers ?? {}) }; - - init.headers = headers; - if (option.data) { - if (option.method === 'GET') throw new Error(`Can not pass data with "GET" method`); - init.body = JSON.stringify(option.data); - - // default fetch content type in request header is json - if (!option.skipContentTypeHack) { - init.headers = { - ...init.headers, - /** - * The "Content-Type" is actually invalid because the body is in json format, - * but it simply adheres to the JKN doc or TrustMark. What a weird API. - */ - 'Content-Type': 'Application/x-www-form-urlencoded' - }; - } - } - - this.onRequest?.({ ...option, type }); - const startedAt = performance.now(); - const response = await fetch(url, init); - result = await response.text(); - if (!result) throw new Error(`The response body is empty (${response.status})`); - - const json: SendResponse[T] = JSON.parse(result); - if (json.response && !option.skipDecrypt) { - const decrypted = this.decrypt(String(json.response), headers['X-timestamp']); - json.response = JSON.parse(this.decompress(decrypted)); - } - - const duration = performance.now() - startedAt; - this.onResponse?.({ ...option, duration, type }, json); - return json; - } catch (error: unknown) { - const customError = new Error( - error instanceof SyntaxError - ? `The response is not JSON (${parseHtml(result)})` - : error instanceof Error - ? error.message - : JSON.stringify(error), - { cause: error } - ); - - this.onError?.(customError); - if (this.config.throw) throw customError; - console.error(customError); - - // TODO: find better way to infer generic response type - const code = type === 'icare' ? 500 : '500'; - const message = `An error occurred: "${customError.message}"`; - return { - metadata: { code: +code, message }, - metaData: { code, message }, - response: undefined - } as unknown as SendResponse[T]; - } - } - - async invalidateConfig() { - this.configured = false; - await this.applyConfig(); - } - - async getConfig(): Promise { - await this.applyConfig(); - return this.config; - } + // simply using custom event function instead of node:EventEmitter + public onRequest: + | ((info: SendOption & { type: Type }) => MaybePromise) + | undefined = undefined; + + public onResponse: + | (( + info: SendOption & { + /** in milliseconds */ duration: number; + type: T; + }, + result: SendResponse[T], + ) => MaybePromise) + | undefined = undefined; + + public onError: ((error: Error) => MaybePromise) | undefined = + undefined; + + private configured = false; + + private config: Config = { + mode: + process.env.NODE_ENV !== "production" + ? "development" + : process.env.NODE_ENV, + ppkCode: process.env.JKN_PPK_CODE ?? "", + consId: process.env.JKN_CONS_ID ?? "", + consSecret: process.env.JKN_CONS_SECRET ?? "", + aplicaresUserKey: process.env.JKN_APLICARES_USER_KEY, + vclaimUserKey: process.env.JKN_VCLAIM_USER_KEY ?? "", + antreanUserKey: process.env.JKN_ANTREAN_USER_KEY ?? "", + apotekUserKey: process.env.JKN_APOTEK_USER_KEY, + pcareUserKey: process.env.JKN_PCARE_USER_KEY ?? "", + pcareUserPass: process.env.JKN_PCARE_USER_PASS ?? "", + icareUserKey: process.env.JKN_ICARE_USER_KEY, + rekamMedisUserKey: process.env.JKN_REKAM_MEDIS_USER_KEY, + throw: false, + baseUrls: defaultBaseUrls, + }; + + constructor( + private userConfig?: + | Partial + | (() => MaybePromise>), + ) {} + + private async applyConfig() { + if (this.configured) return; + + if (typeof this.userConfig === "object") { + this.config = this.mergeConfig(this.config, this.userConfig); + } else if (typeof this.userConfig === "function") { + const userConfig = await this.userConfig(); + this.config = this.mergeConfig(this.config, userConfig); + } + + if (!this.config.consId || !this.config.consSecret) { + throw new Error(`cons id and secret are not defined`); + } + + // fallback to vclaimUserKey + for (const key of [ + "aplicaresUserKey", + "apotekUserKey", + "icareUserKey", + "rekamMedisUserKey", + ] satisfies (keyof Config)[]) { + if (this.config[key]) continue; + this.config[key] = this.config.vclaimUserKey; + } + + this.configured = true; + } + + private mergeConfig(target: Config, source: Partial): Config { + // simple object merge strategy because only baseUrls is typeof object for now + const baseUrls = { ...target.baseUrls, ...source.baseUrls }; + return { ...target, ...source, baseUrls }; + } + + private get userKeyMap(): Record { + return { + aplicares: this.config.aplicaresUserKey, + vclaim: this.config.vclaimUserKey, + antrean: this.config.antreanUserKey, + apotek: this.config.apotekUserKey, + pcare: this.config.pcareUserKey, + icare: this.config.icareUserKey, + rekamMedis: this.config.rekamMedisUserKey, + }; + } + + private getDefaultHeaders(type: Type) { + const userKey = this.userKeyMap[type]; + if (!userKey) throw new Error(`failed to get user key of type "${type}"`); + + const { consId, consSecret } = this.config; + const timestamp = Math.round(Date.now() / 1000); + const message = `${consId}&${timestamp}`; + const signature = createHmac("SHA256", consSecret) + .update(message) + .digest("base64"); + return { + "X-cons-id": consId, + "X-timestamp": String(timestamp), + "X-signature": encodeURI(signature), + user_key: userKey, + }; + } + + private decrypt(responseText: string, requestTimestamp: string) { + const { consId, consSecret } = this.config; + const keyPlain = `${consId}${consSecret}${requestTimestamp}`; + const key = createHash("sha256").update(keyPlain, "utf8").digest(); + const iv = Uint8Array.from(key.subarray(0, 16)); + const decipher = createDecipheriv("aes-256-cbc", key, iv); + let dec = decipher.update(responseText, "base64", "utf8"); + dec += decipher.final("utf8"); + return dec; + } + + private decompress(text: string): string { + return lz.decompressFromEncodedURIComponent(text); + } + + async send( + type: T, + option: SendOption, + ): Promise[T]> { + await this.applyConfig(); + + const path = normalizePath(option.path); + const baseUrl = this.config.baseUrls[type]; + if (!baseUrl) throw new Error(`Invalid base URL for type "${type}"`); + + let result = ""; + try { + const url = new URL(baseUrl[this.config.mode] + path); + const init: RequestInit = { method: option.method ?? "GET" }; + const headers = { + ...this.getDefaultHeaders(type), + ...(option.headers ?? {}), + }; + + init.headers = headers; + if (option.data) { + if (option.method === "GET") + throw new Error(`Can not pass data with "GET" method`); + init.body = JSON.stringify(option.data); + + // default fetch content type in request header is json + if (!option.skipContentTypeHack) { + init.headers = { + ...init.headers, + /** + * The "Content-Type" is actually invalid because the body is in json format, + * but it simply adheres to the JKN doc or TrustMark. What a weird API. + */ + "Content-Type": "Application/x-www-form-urlencoded", + }; + } + } + + this.onRequest?.({ ...option, type }); + const startedAt = performance.now(); + const response = await fetch(url, init); + result = await response.text(); + if (!result) + throw new Error(`The response body is empty (${response.status})`); + + const json: SendResponse[T] = JSON.parse(result); + if (json.response && !option.skipDecrypt) { + const decrypted = this.decrypt( + String(json.response), + headers["X-timestamp"], + ); + json.response = JSON.parse(this.decompress(decrypted)); + } + + const duration = performance.now() - startedAt; + this.onResponse?.({ ...option, duration, type }, json); + return json; + } catch (error: unknown) { + const customError = new Error( + error instanceof SyntaxError + ? `The response is not JSON (${parseHtml(result)})` + : error instanceof Error + ? error.message + : JSON.stringify(error), + { cause: error }, + ); + + this.onError?.(customError); + if (this.config.throw) throw customError; + console.error(customError); + + // TODO: find better way to infer generic response type + const code = type === "icare" ? 500 : "500"; + const message = `An error occurred: "${customError.message}"`; + return { + metadata: { code: +code, message }, + metaData: { code, message }, + response: undefined, + } as unknown as SendResponse[T]; + } + } + + async invalidateConfig() { + this.configured = false; + await this.applyConfig(); + } + + async getConfig(): Promise { + await this.applyConfig(); + return this.config; + } } /** @@ -404,37 +441,40 @@ export class Fetcher { * don't hurt your eyes anymore. */ function parseHtml(html?: string) { - if (!html) return '[empty]'; - return html - .replace(/]*>[\s\S]*?<\/head>/gi, '') - .replace(//gi, '') - .replace(//gi, '') - .replace(/<[^>]*>/g, '') - .trim() - .replace(/\r?\n+/g, ' - ') // newlines to dash - .replace(/\s+/g, ' '); // normalize whitespace + if (!html) return "[empty]"; + return html + .replace(/]*>[\s\S]*?<\/head>/gi, "") + .replace(//gi, "") + .replace(//gi, "") + .replace(/<[^>]*>/g, "") + .trim() + .replace(/\r?\n+/g, " - ") // newlines to dash + .replace(/\s+/g, " "); // normalize whitespace } /** @internal */ -export function normalizePath(path: SendOption['path']) { - const [pathname, params] = typeof path == 'string' ? [path] : path; - - if (!pathname.startsWith('/')) throw new Error(`Path must start with "/"`); - if (!params) return pathname; - - return pathname.replace(/\/:([A-Za-z0-9_]+)(\?)?/g, (_, key: string, optional?: string) => { - const value = params[key]; - - if (value == null) { - if (optional) return ''; // remove entire `/:param?` - throw new Error(`Missing params: ${key}`); - } - - const component = String(value); - try { - return '/' + encodeURIComponent(decodeURIComponent(component)); - } catch { - return '/' + encodeURIComponent(component); - } - }); +export function normalizePath(path: SendOption["path"]) { + const [pathname, params] = typeof path == "string" ? [path] : path; + + if (!pathname.startsWith("/")) throw new Error(`Path must start with "/"`); + if (!params) return pathname; + + return pathname.replace( + /\/:([A-Za-z0-9_]+)(\?)?/g, + (_, key: string, optional?: string) => { + const value = params[key]; + + if (value == null) { + if (optional) return ""; // remove entire `/:param?` + throw new Error(`Missing params: ${key}`); + } + + const component = String(value); + try { + return "/" + encodeURIComponent(decodeURIComponent(component)); + } catch { + return "/" + encodeURIComponent(component); + } + }, + ); } diff --git a/src/pcare/diagnosa.ts b/src/pcare/diagnosa.ts new file mode 100644 index 0000000..2945343 --- /dev/null +++ b/src/pcare/diagnosa.ts @@ -0,0 +1,17 @@ +import { PCareBaseApi } from "./base.js"; + +export class Diagnosa extends PCareBaseApi { + async get(params: { kode: string; row: number; limit: number }) { + return this.send<{ count: number; list: DiagnosaResult[] }>({ + name: this.name + "Data Diagnosa", + path: ["/diagnosa/:kode/:row/:limit", params], + method: "GET", + }); + } +} + +interface DiagnosaResult { + kdDiag: string; + nmDiag: string; + nonSpesialis: boolean; +} diff --git a/src/pcare/index.ts b/src/pcare/index.ts index 7b6b328..3bf7e81 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -1,13 +1,14 @@ -import { SendOption } from '../fetcher.js'; -import { PCareBaseApi } from './base.js'; +import { CachedApi } from "../base.js"; -type RequestOption = SendOption; +export class PCare { + private static instance: PCare | undefined; -/** - * Implement base api directly since this is for partial support - */ -export class PCare extends PCareBaseApi { - async request(option: RequestOption) { - return this.send(option); - } + private constructor(private readonly cache: CachedApi) {} + + static getInstance(cache: CachedApi): PCare { + if (!this.instance) { + this.instance = new PCare(cache); + } + return this.instance; + } } From 29a268b3db720f7da7a55d8c17b09302d2738a28 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:07:10 +0800 Subject: [PATCH 02/29] fix: add config for user and pass pcare --- src/fetcher.ts | 864 ++++++++++++++++++++++++------------------------- 1 file changed, 419 insertions(+), 445 deletions(-) diff --git a/src/fetcher.ts b/src/fetcher.ts index 13dfab7..acd7755 100644 --- a/src/fetcher.ts +++ b/src/fetcher.ts @@ -1,439 +1,416 @@ -import lz from "lz-string"; -import { createDecipheriv, createHash, createHmac } from "node:crypto"; +import lz from 'lz-string'; +import { createDecipheriv, createHash, createHmac } from 'node:crypto'; type MaybePromise = T | Promise; -export type Mode = "development" | "production"; +export type Mode = 'development' | 'production'; -export type Type = - | "aplicares" - | "vclaim" - | "antrean" - | "apotek" - | "pcare" - | "icare" - | "rekamMedis"; +export type Type = 'aplicares' | 'vclaim' | 'antrean' | 'apotek' | 'pcare' | 'icare' | 'rekamMedis'; export interface Config { - /** - * Kode PPK yang diberikan BPJS. - * - * Diperlukan untuk melakukan proses enkripsi - * pada service eRekamMedis and request pada - * service Aplicares - * - * @default process.env.JKN_PPK_CODE - */ - ppkCode: string; - - /** - * Cons ID dari BPJS - * - * @default process.env.JKN_CONS_ID - */ - consId: string; - - /** - * Secret key dari BPJS - * - * @default process.env.JKN_CONS_SECRET - */ - consSecret: string; - - /** - * User key Aplicares dari BPJS atau gunakan user key vclaim - * - * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_APLICARES_USER_KEY - */ - aplicaresUserKey?: string; - - /** - * User key VClaim dari BPJS - * - * @default process.env.JKN_VCLAIM_USER_KEY - */ - vclaimUserKey: string; - - /** - * User key Antrean dari BPJS - * - * @default process.env.JKN_ANTREAN_USER_KEY - */ - antreanUserKey: string; - - /** - * User key Apotek dari BPJS atau gunakan user key vclaim - * - * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_APOTEK_USER_KEY - */ - apotekUserKey?: string; - - /** - * User key PCare dari BPJS - * - * @default process.env.JKN_PCARE_USER_KEY - */ - pcareUserKey: string; - - /** - * User dan Password PCare dari BPJS - * contoh: username:password - * - * @default process.env.JKN_PCARE_USER_PASS - */ - pcareUserPass: string; - - /** - * User key i-Care dari BPJS - * - * Umumnya user key i-Care ini nilai sama dengan user key VClaim - * untuk FKRTL dan PCare untuk FKTP - * - * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_ICARE_USER_KEY - */ - icareUserKey?: string; - - /** - * User key eRekam Medis dari BPJS atau gunakan user key vclaim - * - * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_REKAM_MEDIS_USER_KEY - */ - rekamMedisUserKey?: string; - - /** - * Berupa mode "development" dan "production". Secara default akan - * membaca nilai environment variable NODE_ENV atau "development" - * jika NODE_ENV tidak terdapat nilai. Mode ini berpengaruh pada - * nilai konfigurasi yang digunakan dan JKN API base url. - * - * @default process.env.NODE_ENV || "development" - */ - mode: Mode; - - /** - * Secara default bernilai `false` sehingga setiap terjadi kesalahan - * saat mengirim permintaan ke server JKN menggunakan method `send()`, - * pesan kesalahan akan dikembalikan sebagai pesan response dan log - * error akan dicetak pada konsol atau terminal. Jika bernilai true, - * maka kesalahan akan di-throw. - * - * @default false - */ - throw: boolean; - - /** - * Base URL web service dari BPJS. Secara default sudah diatur - * berdasarkan base url yang ada di TrustMark. Nilai dapat diatur - * secara partial, misalnya: - * - * ``` - * baseUrls: { - * vclaim: { - * development: 'http://dev.example.com', - * production: 'http://prod.example.com' - * } - * } - * ``` - */ - baseUrls: Partial>>; + /** + * Kode PPK yang diberikan BPJS. + * + * Diperlukan untuk melakukan proses enkripsi + * pada service eRekamMedis and request pada + * service Aplicares + * + * @default process.env.JKN_PPK_CODE + */ + ppkCode: string; + + /** + * Cons ID dari BPJS + * + * @default process.env.JKN_CONS_ID + */ + consId: string; + + /** + * Secret key dari BPJS + * + * @default process.env.JKN_CONS_SECRET + */ + consSecret: string; + + /** + * User key Aplicares dari BPJS atau gunakan user key vclaim + * + * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_APLICARES_USER_KEY + */ + aplicaresUserKey?: string; + + /** + * User key VClaim dari BPJS + * + * @default process.env.JKN_VCLAIM_USER_KEY + */ + vclaimUserKey: string; + + /** + * User key Antrean dari BPJS + * + * @default process.env.JKN_ANTREAN_USER_KEY + */ + antreanUserKey: string; + + /** + * User key Apotek dari BPJS atau gunakan user key vclaim + * + * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_APOTEK_USER_KEY + */ + apotekUserKey?: string; + + /** + * User key PCare dari BPJS + * + * @default process.env.JKN_PCARE_USER_KEY + */ + pcareUserKey: string; + + /** + * User dan Password PCare dari BPJS + * contoh: username:password + * + * @default process.env.JKN_PCARE_USER_PASS + */ + pcareUserPass: string; + + /** + * User key i-Care dari BPJS + * + * Umumnya user key i-Care ini nilai sama dengan user key VClaim + * untuk FKRTL dan PCare untuk FKTP + * + * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_ICARE_USER_KEY + */ + icareUserKey?: string; + + /** + * User key eRekam Medis dari BPJS atau gunakan user key vclaim + * + * @default process.env.JKN_VCLAIM_USER_KEY || process.env.JKN_REKAM_MEDIS_USER_KEY + */ + rekamMedisUserKey?: string; + + /** + * Berupa mode "development" dan "production". Secara default akan + * membaca nilai environment variable NODE_ENV atau "development" + * jika NODE_ENV tidak terdapat nilai. Mode ini berpengaruh pada + * nilai konfigurasi yang digunakan dan JKN API base url. + * + * @default process.env.NODE_ENV || "development" + */ + mode: Mode; + + /** + * Secara default bernilai `false` sehingga setiap terjadi kesalahan + * saat mengirim permintaan ke server JKN menggunakan method `send()`, + * pesan kesalahan akan dikembalikan sebagai pesan response dan log + * error akan dicetak pada konsol atau terminal. Jika bernilai true, + * maka kesalahan akan di-throw. + * + * @default false + */ + throw: boolean; + + /** + * Base URL web service dari BPJS. Secara default sudah diatur + * berdasarkan base url yang ada di TrustMark. Nilai dapat diatur + * secara partial, misalnya: + * + * ``` + * baseUrls: { + * vclaim: { + * development: 'http://dev.example.com', + * production: 'http://prod.example.com' + * } + * } + * ``` + */ + baseUrls: Partial>>; } export interface SendOption { - /** name of request, it helpful for log or collect stats */ - name?: string; - path: - | `/${string}` - | [`/${string}`, Record]; - method?: "GET" | "POST" | "PUT" | "DELETE"; - data?: unknown; - skipDecrypt?: boolean; - headers?: Record; - - /** - * Mengatur "Content-Type" pada request header menjadi "application/json; charset=utf-8". - * Secara default "Content-Type" pada request header adalah "application/x-www-form-urlencoded" - * meski pun saat request nilai body dalam format json. Ini tidak standar, namun mengacu pada - * panduan bridging pada halaman dokumentasi di situs TrustMark yang memaksa hal tersebut. - * - * @default undefined - */ - skipContentTypeHack?: boolean; + /** name of request, it helpful for log or collect stats */ + name?: string; + path: `/${string}` | [`/${string}`, Record]; + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + data?: unknown; + skipDecrypt?: boolean; + headers?: Record; + + /** + * Mengatur "Content-Type" pada request header menjadi "application/json; charset=utf-8". + * Secara default "Content-Type" pada request header adalah "application/x-www-form-urlencoded" + * meski pun saat request nilai body dalam format json. Ini tidak standar, namun mengacu pada + * panduan bridging pada halaman dokumentasi di situs TrustMark yang memaksa hal tersebut. + * + * @default undefined + */ + skipContentTypeHack?: boolean; } export interface LowerResponse { - response: T; - metadata: { - code: C; - message: string; - } & E; + response: T; + metadata: { + code: C; + message: string; + } & E; } export interface CamelResponse { - response: T; - metaData: { - code: C; - message: string; - } & E; + response: T; + metaData: { + code: C; + message: string; + } & E; } export type SendResponse = { - aplicares: LowerResponse; - antrean: LowerResponse; - vclaim: CamelResponse; - apotek: CamelResponse; - pcare: CamelResponse; - icare: CamelResponse; - rekamMedis: LowerResponse; + aplicares: LowerResponse; + antrean: LowerResponse; + vclaim: CamelResponse; + apotek: CamelResponse; + pcare: CamelResponse; + icare: CamelResponse; + rekamMedis: LowerResponse; }; const defaultBaseUrls: Record> = { - aplicares: { - development: "https://dvlp.bpjs-kesehatan.go.id:8888/aplicaresws", - production: "https://new-api.bpjs-kesehatan.go.id/aplicaresws", - }, - vclaim: { - development: "https://apijkn-dev.bpjs-kesehatan.go.id/vclaim-rest-dev", - production: "https://apijkn.bpjs-kesehatan.go.id/vclaim-rest", - }, - antrean: { - development: "https://apijkn-dev.bpjs-kesehatan.go.id/antreanrs_dev", - production: "https://apijkn.bpjs-kesehatan.go.id/antreanrs", - }, - apotek: { - development: "https://apijkn-dev.bpjs-kesehatan.go.id/apotek-rest-dev", - production: "https://apijkn.bpjs-kesehatan.go.id/apotek-rest", - }, - pcare: { - development: "https://apijkn-dev.bpjs-kesehatan.go.id/pcare-rest-dev", - production: "https://apijkn.bpjs-kesehatan.go.id/pcare-rest", - }, - icare: { - development: "https://apijkn-dev.bpjs-kesehatan.go.id/ihs_dev", - production: "https://apijkn.bpjs-kesehatan.go.id/wsihs", - }, - rekamMedis: { - development: "https://apijkn-dev.bpjs-kesehatan.go.id/erekammedis_dev", - production: "https://apijkn.bpjs-kesehatan.go.id/medicalrecord", - }, + aplicares: { + development: 'https://dvlp.bpjs-kesehatan.go.id:8888/aplicaresws', + production: 'https://new-api.bpjs-kesehatan.go.id/aplicaresws' + }, + vclaim: { + development: 'https://apijkn-dev.bpjs-kesehatan.go.id/vclaim-rest-dev', + production: 'https://apijkn.bpjs-kesehatan.go.id/vclaim-rest' + }, + antrean: { + development: 'https://apijkn-dev.bpjs-kesehatan.go.id/antreanrs_dev', + production: 'https://apijkn.bpjs-kesehatan.go.id/antreanrs' + }, + apotek: { + development: 'https://apijkn-dev.bpjs-kesehatan.go.id/apotek-rest-dev', + production: 'https://apijkn.bpjs-kesehatan.go.id/apotek-rest' + }, + pcare: { + development: 'https://apijkn-dev.bpjs-kesehatan.go.id/pcare-rest-dev', + production: 'https://apijkn.bpjs-kesehatan.go.id/pcare-rest' + }, + icare: { + development: 'https://apijkn-dev.bpjs-kesehatan.go.id/ihs_dev', + production: 'https://apijkn.bpjs-kesehatan.go.id/wsihs' + }, + rekamMedis: { + development: 'https://apijkn-dev.bpjs-kesehatan.go.id/erekammedis_dev', + production: 'https://apijkn.bpjs-kesehatan.go.id/medicalrecord' + } }; export class Fetcher { - // simply using custom event function instead of node:EventEmitter - public onRequest: - | ((info: SendOption & { type: Type }) => MaybePromise) - | undefined = undefined; - - public onResponse: - | (( - info: SendOption & { - /** in milliseconds */ duration: number; - type: T; - }, - result: SendResponse[T], - ) => MaybePromise) - | undefined = undefined; - - public onError: ((error: Error) => MaybePromise) | undefined = - undefined; - - private configured = false; - - private config: Config = { - mode: - process.env.NODE_ENV !== "production" - ? "development" - : process.env.NODE_ENV, - ppkCode: process.env.JKN_PPK_CODE ?? "", - consId: process.env.JKN_CONS_ID ?? "", - consSecret: process.env.JKN_CONS_SECRET ?? "", - aplicaresUserKey: process.env.JKN_APLICARES_USER_KEY, - vclaimUserKey: process.env.JKN_VCLAIM_USER_KEY ?? "", - antreanUserKey: process.env.JKN_ANTREAN_USER_KEY ?? "", - apotekUserKey: process.env.JKN_APOTEK_USER_KEY, - pcareUserKey: process.env.JKN_PCARE_USER_KEY ?? "", - pcareUserPass: process.env.JKN_PCARE_USER_PASS ?? "", - icareUserKey: process.env.JKN_ICARE_USER_KEY, - rekamMedisUserKey: process.env.JKN_REKAM_MEDIS_USER_KEY, - throw: false, - baseUrls: defaultBaseUrls, - }; - - constructor( - private userConfig?: - | Partial - | (() => MaybePromise>), - ) {} - - private async applyConfig() { - if (this.configured) return; - - if (typeof this.userConfig === "object") { - this.config = this.mergeConfig(this.config, this.userConfig); - } else if (typeof this.userConfig === "function") { - const userConfig = await this.userConfig(); - this.config = this.mergeConfig(this.config, userConfig); - } - - if (!this.config.consId || !this.config.consSecret) { - throw new Error(`cons id and secret are not defined`); - } - - // fallback to vclaimUserKey - for (const key of [ - "aplicaresUserKey", - "apotekUserKey", - "icareUserKey", - "rekamMedisUserKey", - ] satisfies (keyof Config)[]) { - if (this.config[key]) continue; - this.config[key] = this.config.vclaimUserKey; - } - - this.configured = true; - } - - private mergeConfig(target: Config, source: Partial): Config { - // simple object merge strategy because only baseUrls is typeof object for now - const baseUrls = { ...target.baseUrls, ...source.baseUrls }; - return { ...target, ...source, baseUrls }; - } - - private get userKeyMap(): Record { - return { - aplicares: this.config.aplicaresUserKey, - vclaim: this.config.vclaimUserKey, - antrean: this.config.antreanUserKey, - apotek: this.config.apotekUserKey, - pcare: this.config.pcareUserKey, - icare: this.config.icareUserKey, - rekamMedis: this.config.rekamMedisUserKey, - }; - } - - private getDefaultHeaders(type: Type) { - const userKey = this.userKeyMap[type]; - if (!userKey) throw new Error(`failed to get user key of type "${type}"`); - - const { consId, consSecret } = this.config; - const timestamp = Math.round(Date.now() / 1000); - const message = `${consId}&${timestamp}`; - const signature = createHmac("SHA256", consSecret) - .update(message) - .digest("base64"); - return { - "X-cons-id": consId, - "X-timestamp": String(timestamp), - "X-signature": encodeURI(signature), - user_key: userKey, - }; - } - - private decrypt(responseText: string, requestTimestamp: string) { - const { consId, consSecret } = this.config; - const keyPlain = `${consId}${consSecret}${requestTimestamp}`; - const key = createHash("sha256").update(keyPlain, "utf8").digest(); - const iv = Uint8Array.from(key.subarray(0, 16)); - const decipher = createDecipheriv("aes-256-cbc", key, iv); - let dec = decipher.update(responseText, "base64", "utf8"); - dec += decipher.final("utf8"); - return dec; - } - - private decompress(text: string): string { - return lz.decompressFromEncodedURIComponent(text); - } - - async send( - type: T, - option: SendOption, - ): Promise[T]> { - await this.applyConfig(); - - const path = normalizePath(option.path); - const baseUrl = this.config.baseUrls[type]; - if (!baseUrl) throw new Error(`Invalid base URL for type "${type}"`); - - let result = ""; - try { - const url = new URL(baseUrl[this.config.mode] + path); - const init: RequestInit = { method: option.method ?? "GET" }; - const headers = { - ...this.getDefaultHeaders(type), - ...(option.headers ?? {}), - }; - - init.headers = headers; - if (option.data) { - if (option.method === "GET") - throw new Error(`Can not pass data with "GET" method`); - init.body = JSON.stringify(option.data); - - // default fetch content type in request header is json - if (!option.skipContentTypeHack) { - init.headers = { - ...init.headers, - /** - * The "Content-Type" is actually invalid because the body is in json format, - * but it simply adheres to the JKN doc or TrustMark. What a weird API. - */ - "Content-Type": "Application/x-www-form-urlencoded", - }; - } - } - - this.onRequest?.({ ...option, type }); - const startedAt = performance.now(); - const response = await fetch(url, init); - result = await response.text(); - if (!result) - throw new Error(`The response body is empty (${response.status})`); - - const json: SendResponse[T] = JSON.parse(result); - if (json.response && !option.skipDecrypt) { - const decrypted = this.decrypt( - String(json.response), - headers["X-timestamp"], - ); - json.response = JSON.parse(this.decompress(decrypted)); - } - - const duration = performance.now() - startedAt; - this.onResponse?.({ ...option, duration, type }, json); - return json; - } catch (error: unknown) { - const customError = new Error( - error instanceof SyntaxError - ? `The response is not JSON (${parseHtml(result)})` - : error instanceof Error - ? error.message - : JSON.stringify(error), - { cause: error }, - ); - - this.onError?.(customError); - if (this.config.throw) throw customError; - console.error(customError); - - // TODO: find better way to infer generic response type - const code = type === "icare" ? 500 : "500"; - const message = `An error occurred: "${customError.message}"`; - return { - metadata: { code: +code, message }, - metaData: { code, message }, - response: undefined, - } as unknown as SendResponse[T]; - } - } - - async invalidateConfig() { - this.configured = false; - await this.applyConfig(); - } - - async getConfig(): Promise { - await this.applyConfig(); - return this.config; - } + // simply using custom event function instead of node:EventEmitter + public onRequest: ((info: SendOption & { type: Type }) => MaybePromise) | undefined = + undefined; + + public onResponse: + | (( + info: SendOption & { + /** in milliseconds */ duration: number; + type: T; + }, + result: SendResponse[T] + ) => MaybePromise) + | undefined = undefined; + + public onError: ((error: Error) => MaybePromise) | undefined = undefined; + + private configured = false; + + private config: Config = { + mode: process.env.NODE_ENV !== 'production' ? 'development' : process.env.NODE_ENV, + ppkCode: process.env.JKN_PPK_CODE ?? '', + consId: process.env.JKN_CONS_ID ?? '', + consSecret: process.env.JKN_CONS_SECRET ?? '', + aplicaresUserKey: process.env.JKN_APLICARES_USER_KEY, + vclaimUserKey: process.env.JKN_VCLAIM_USER_KEY ?? '', + antreanUserKey: process.env.JKN_ANTREAN_USER_KEY ?? '', + apotekUserKey: process.env.JKN_APOTEK_USER_KEY, + pcareUserKey: process.env.JKN_PCARE_USER_KEY ?? '', + pcareUserPass: process.env.JKN_PCARE_USER_PASS ?? '', + icareUserKey: process.env.JKN_ICARE_USER_KEY, + rekamMedisUserKey: process.env.JKN_REKAM_MEDIS_USER_KEY, + throw: false, + baseUrls: defaultBaseUrls + }; + + constructor(private userConfig?: Partial | (() => MaybePromise>)) {} + + private async applyConfig() { + if (this.configured) return; + + if (typeof this.userConfig === 'object') { + this.config = this.mergeConfig(this.config, this.userConfig); + } else if (typeof this.userConfig === 'function') { + const userConfig = await this.userConfig(); + this.config = this.mergeConfig(this.config, userConfig); + } + + if (!this.config.consId || !this.config.consSecret) { + throw new Error(`cons id and secret are not defined`); + } + + // fallback to vclaimUserKey + for (const key of [ + 'aplicaresUserKey', + 'apotekUserKey', + 'icareUserKey', + 'rekamMedisUserKey' + ] satisfies (keyof Config)[]) { + if (this.config[key]) continue; + this.config[key] = this.config.vclaimUserKey; + } + + this.configured = true; + } + + private mergeConfig(target: Config, source: Partial): Config { + // simple object merge strategy because only baseUrls is typeof object for now + const baseUrls = { ...target.baseUrls, ...source.baseUrls }; + return { ...target, ...source, baseUrls }; + } + + private get userKeyMap(): Record { + return { + aplicares: this.config.aplicaresUserKey, + vclaim: this.config.vclaimUserKey, + antrean: this.config.antreanUserKey, + apotek: this.config.apotekUserKey, + pcare: this.config.pcareUserKey, + icare: this.config.icareUserKey, + rekamMedis: this.config.rekamMedisUserKey + }; + } + + private getDefaultHeaders(type: Type) { + const userKey = this.userKeyMap[type]; + if (!userKey) throw new Error(`failed to get user key of type "${type}"`); + + const { consId, consSecret, pcareUserPass } = this.config; + const timestamp = Math.round(Date.now() / 1000); + const message = `${consId}&${timestamp}`; + const signature = createHmac('SHA256', consSecret).update(message).digest('base64'); + const pcareAuth = `Basic ${Buffer.from(`${pcareUserPass}:095`).toString('base64')}`; + return { + 'X-cons-id': consId, + 'X-timestamp': String(timestamp), + 'X-signature': encodeURI(signature), + user_key: userKey, + 'X-Authorization': pcareAuth + }; + } + + private decrypt(responseText: string, requestTimestamp: string) { + const { consId, consSecret } = this.config; + const keyPlain = `${consId}${consSecret}${requestTimestamp}`; + const key = createHash('sha256').update(keyPlain, 'utf8').digest(); + const iv = Uint8Array.from(key.subarray(0, 16)); + const decipher = createDecipheriv('aes-256-cbc', key, iv); + let dec = decipher.update(responseText, 'base64', 'utf8'); + dec += decipher.final('utf8'); + return dec; + } + + private decompress(text: string): string { + return lz.decompressFromEncodedURIComponent(text); + } + + async send( + type: T, + option: SendOption + ): Promise[T]> { + await this.applyConfig(); + + const path = normalizePath(option.path); + const baseUrl = this.config.baseUrls[type]; + if (!baseUrl) throw new Error(`Invalid base URL for type "${type}"`); + + let result = ''; + try { + const url = new URL(baseUrl[this.config.mode] + path); + const init: RequestInit = { method: option.method ?? 'GET' }; + const headers = { + ...this.getDefaultHeaders(type), + ...(option.headers ?? {}) + }; + + init.headers = headers; + if (option.data) { + if (option.method === 'GET') throw new Error(`Can not pass data with "GET" method`); + init.body = JSON.stringify(option.data); + + // default fetch content type in request header is json + if (!option.skipContentTypeHack) { + init.headers = { + ...init.headers, + /** + * The "Content-Type" is actually invalid because the body is in json format, + * but it simply adheres to the JKN doc or TrustMark. What a weird API. + */ + 'Content-Type': 'Application/x-www-form-urlencoded' + }; + } + } + + this.onRequest?.({ ...option, type }); + const startedAt = performance.now(); + const response = await fetch(url, init); + result = await response.text(); + if (!result) throw new Error(`The response body is empty (${response.status})`); + + const json: SendResponse[T] = JSON.parse(result); + if (json.response && !option.skipDecrypt) { + const decrypted = this.decrypt(String(json.response), headers['X-timestamp']); + json.response = JSON.parse(this.decompress(decrypted)); + } + + const duration = performance.now() - startedAt; + this.onResponse?.({ ...option, duration, type }, json); + return json; + } catch (error: unknown) { + const customError = new Error( + error instanceof SyntaxError + ? `The response is not JSON (${parseHtml(result)})` + : error instanceof Error + ? error.message + : JSON.stringify(error), + { cause: error } + ); + + this.onError?.(customError); + if (this.config.throw) throw customError; + console.error(customError); + + // TODO: find better way to infer generic response type + const code = type === 'icare' ? 500 : '500'; + const message = `An error occurred: "${customError.message}"`; + return { + metadata: { code: +code, message }, + metaData: { code, message }, + response: undefined + } as unknown as SendResponse[T]; + } + } + + async invalidateConfig() { + this.configured = false; + await this.applyConfig(); + } + + async getConfig(): Promise { + await this.applyConfig(); + return this.config; + } } /** @@ -441,40 +418,37 @@ export class Fetcher { * don't hurt your eyes anymore. */ function parseHtml(html?: string) { - if (!html) return "[empty]"; - return html - .replace(/]*>[\s\S]*?<\/head>/gi, "") - .replace(//gi, "") - .replace(//gi, "") - .replace(/<[^>]*>/g, "") - .trim() - .replace(/\r?\n+/g, " - ") // newlines to dash - .replace(/\s+/g, " "); // normalize whitespace + if (!html) return '[empty]'; + return html + .replace(/]*>[\s\S]*?<\/head>/gi, '') + .replace(//gi, '') + .replace(//gi, '') + .replace(/<[^>]*>/g, '') + .trim() + .replace(/\r?\n+/g, ' - ') // newlines to dash + .replace(/\s+/g, ' '); // normalize whitespace } /** @internal */ -export function normalizePath(path: SendOption["path"]) { - const [pathname, params] = typeof path == "string" ? [path] : path; - - if (!pathname.startsWith("/")) throw new Error(`Path must start with "/"`); - if (!params) return pathname; - - return pathname.replace( - /\/:([A-Za-z0-9_]+)(\?)?/g, - (_, key: string, optional?: string) => { - const value = params[key]; - - if (value == null) { - if (optional) return ""; // remove entire `/:param?` - throw new Error(`Missing params: ${key}`); - } - - const component = String(value); - try { - return "/" + encodeURIComponent(decodeURIComponent(component)); - } catch { - return "/" + encodeURIComponent(component); - } - }, - ); +export function normalizePath(path: SendOption['path']) { + const [pathname, params] = typeof path === 'string' ? [path] : path; + + if (!pathname.startsWith('/')) throw new Error(`Path must start with "/"`); + if (!params) return pathname; + + return pathname.replace(/\/:([A-Za-z0-9_]+)(\?)?/g, (_, key: string, optional?: string) => { + const value = params[key]; + + if (value == null) { + if (optional) return ''; // remove entire `/:param?` + throw new Error(`Missing params: ${key}`); + } + + const component = String(value); + try { + return '/' + encodeURIComponent(decodeURIComponent(component)); + } catch { + return '/' + encodeURIComponent(component); + } + }); } From 4cfc392e587540539dc4abd4aa80ef6e375c4de5 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:07:41 +0800 Subject: [PATCH 03/29] fix: update pcare instance --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 227e4bc..50c4f1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ export default class JKN extends Fetcher { } get pcare(): PCare { - return this.cache.get('pcare', PCare); + return PCare.getInstance(this.cache); } get antreanFktp(): AntreanFKTP { From d89a565e0aad60b2404bf0860135224a4e316ec3 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:08:18 +0800 Subject: [PATCH 04/29] feat: add pcare diagnosa --- src/pcare/diagnosa.ts | 36 +++++++++++++++++++++++++----------- src/pcare/index.ts | 23 ++++++++++++++--------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/pcare/diagnosa.ts b/src/pcare/diagnosa.ts index 2945343..69d8891 100644 --- a/src/pcare/diagnosa.ts +++ b/src/pcare/diagnosa.ts @@ -1,17 +1,31 @@ -import { PCareBaseApi } from "./base.js"; +import { PCareBaseApi } from './base.js'; export class Diagnosa extends PCareBaseApi { - async get(params: { kode: string; row: number; limit: number }) { - return this.send<{ count: number; list: DiagnosaResult[] }>({ - name: this.name + "Data Diagnosa", - path: ["/diagnosa/:kode/:row/:limit", params], - method: "GET", - }); - } + async get(params: { + /** Kode atau nama diagnosa */ + kode: string; + + /** Row data awal yang akan ditampilkan */ + row: number; + + /** Limit jumlah data yang akan ditampilkan */ + limit: number; + }) { + return this.send<{ count: number; list: DiagnosaResult[] }>({ + name: `${this.name} Data Diagnosa`, + path: ['/diagnosa/:kode/:row/:limit', params], + method: 'GET' + }); + } } interface DiagnosaResult { - kdDiag: string; - nmDiag: string; - nonSpesialis: boolean; + /** kode diagnosa */ + kdDiag: string; + + /** nama diagnonsa */ + nmDiag: string; + + /** non-spesialis */ + nonSpesialis: boolean; } diff --git a/src/pcare/index.ts b/src/pcare/index.ts index 3bf7e81..a1c7d54 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -1,14 +1,19 @@ -import { CachedApi } from "../base.js"; +import { CachedApi } from '../base.js'; +import { Diagnosa } from './diagnosa.js'; export class PCare { - private static instance: PCare | undefined; + private static instance: PCare | undefined; - private constructor(private readonly cache: CachedApi) {} + private constructor(private readonly cache: CachedApi) {} - static getInstance(cache: CachedApi): PCare { - if (!this.instance) { - this.instance = new PCare(cache); - } - return this.instance; - } + static getInstance(cache: CachedApi): PCare { + if (!this.instance) { + this.instance = new PCare(cache); + } + return this.instance; + } + + get diagnosa() { + return this.cache.get('pcare_diagnosa', Diagnosa); + } } From d9378d0305f451aa0a0ef635eff58ba6e8a13933 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:09:06 +0800 Subject: [PATCH 05/29] feat(test): add file test for pcare diagnosa --- test/pcare/diagnosa.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 test/pcare/diagnosa.test.ts diff --git a/test/pcare/diagnosa.test.ts b/test/pcare/diagnosa.test.ts new file mode 100644 index 0000000..a88e948 --- /dev/null +++ b/test/pcare/diagnosa.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Diagnnosa', { timeout: 25_000 }, () => { + it.concurrent('dataDiagnosa()', async () => { + const result = await jkn.pcare.diagnosa.get({ kode: 'A00', row: 1, limit: 10 }); + console.log(result); + expect(result.metaData.code).toBe(200); // no example data + }); +}); From ad4f036ec25332e4a8e90794b40df48e84fc476a Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:19:15 +0800 Subject: [PATCH 06/29] feat: add pcare get data dokter --- src/pcare/dokter.ts | 25 +++++++++++++++++++++++++ src/pcare/index.ts | 5 +++++ test/pcare/diagnosa.test.ts | 1 - 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/pcare/dokter.ts diff --git a/src/pcare/dokter.ts b/src/pcare/dokter.ts new file mode 100644 index 0000000..49f83bc --- /dev/null +++ b/src/pcare/dokter.ts @@ -0,0 +1,25 @@ +import { PCareBaseApi } from './base.js'; + +export class Dokter extends PCareBaseApi { + async get(params: { + /** Row data awal yang akan ditampilkan */ + row: number; + + /** Limit jumlah data yang akan ditampilkan */ + limit: number; + }) { + return this.send<{ count: number; list: DokterResult[] }>({ + name: `${this.name} Data Dokter`, + path: ['/dokter/:row/:limit', params], + method: 'GET' + }); + } +} + +interface DokterResult { + /** kode dokter */ + kdDokter: string; + + /** nama dokter */ + nmDokter: string; +} diff --git a/src/pcare/index.ts b/src/pcare/index.ts index a1c7d54..c38ddd0 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -1,5 +1,6 @@ import { CachedApi } from '../base.js'; import { Diagnosa } from './diagnosa.js'; +import { Dokter } from './dokter.js'; export class PCare { private static instance: PCare | undefined; @@ -16,4 +17,8 @@ export class PCare { get diagnosa() { return this.cache.get('pcare_diagnosa', Diagnosa); } + + get dokter() { + return this.cache.get('pcare_dokter', Dokter); + } } diff --git a/test/pcare/diagnosa.test.ts b/test/pcare/diagnosa.test.ts index a88e948..4334f20 100644 --- a/test/pcare/diagnosa.test.ts +++ b/test/pcare/diagnosa.test.ts @@ -4,7 +4,6 @@ import jkn from '../jkn'; describe('PCare - Diagnnosa', { timeout: 25_000 }, () => { it.concurrent('dataDiagnosa()', async () => { const result = await jkn.pcare.diagnosa.get({ kode: 'A00', row: 1, limit: 10 }); - console.log(result); expect(result.metaData.code).toBe(200); // no example data }); }); From 30ab86b7143f26395d17acc442676e9f6ee175ad Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:19:41 +0800 Subject: [PATCH 07/29] feat(test): add testing for pcare get data dokter --- test/pcare/dokter.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/pcare/dokter.test.ts diff --git a/test/pcare/dokter.test.ts b/test/pcare/dokter.test.ts new file mode 100644 index 0000000..890ccea --- /dev/null +++ b/test/pcare/dokter.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Dokter', { timeout: 25_000 }, () => { + it.concurrent('dataDokter()', async () => { + const result = await jkn.pcare.dokter.get({ row: 1, limit: 10 }); + expect(result.metaData.code).toBe(200); // no example data + }); +}); From aa60d8bfc441a59e833c89eaef1ae9da064a13c4 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:27:55 +0800 Subject: [PATCH 08/29] fix(docs): add comment docs --- src/pcare/diagnosa.ts | 4 ++++ src/pcare/dokter.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/pcare/diagnosa.ts b/src/pcare/diagnosa.ts index 69d8891..7494baa 100644 --- a/src/pcare/diagnosa.ts +++ b/src/pcare/diagnosa.ts @@ -1,6 +1,10 @@ import { PCareBaseApi } from './base.js'; export class Diagnosa extends PCareBaseApi { + /** + * Get Data Diagnosa + * @param params param {kode, row, limit} + */ async get(params: { /** Kode atau nama diagnosa */ kode: string; diff --git a/src/pcare/dokter.ts b/src/pcare/dokter.ts index 49f83bc..7dce380 100644 --- a/src/pcare/dokter.ts +++ b/src/pcare/dokter.ts @@ -1,6 +1,10 @@ import { PCareBaseApi } from './base.js'; export class Dokter extends PCareBaseApi { + /** + * Get Data Dokter + * @param params params{row,limit} + */ async get(params: { /** Row data awal yang akan ditampilkan */ row: number; From 8a26305ebd39959232818d3ff07d723595bf5cd3 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:28:13 +0800 Subject: [PATCH 09/29] feat: add pcare get data kesadaran --- src/pcare/index.ts | 5 +++++ src/pcare/kesadaran.ts | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/pcare/kesadaran.ts diff --git a/src/pcare/index.ts b/src/pcare/index.ts index c38ddd0..ad7b948 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -1,6 +1,7 @@ import { CachedApi } from '../base.js'; import { Diagnosa } from './diagnosa.js'; import { Dokter } from './dokter.js'; +import { Kesadaran } from './kesadaran.js'; export class PCare { private static instance: PCare | undefined; @@ -21,4 +22,8 @@ export class PCare { get dokter() { return this.cache.get('pcare_dokter', Dokter); } + + get kesadaran() { + return this.cache.get('pcare_kesadaran', Kesadaran); + } } diff --git a/src/pcare/kesadaran.ts b/src/pcare/kesadaran.ts new file mode 100644 index 0000000..d43aba4 --- /dev/null +++ b/src/pcare/kesadaran.ts @@ -0,0 +1,22 @@ +import { PCareBaseApi } from './base.js'; + +export class Kesadaran extends PCareBaseApi { + /** + * Get Data Kesadaran + */ + async get() { + return this.send<{ count: number; list: KesadaranResult[] }>({ + name: `${this.name} Data Kesadaran`, + path: '/kesadaran', + method: 'GET' + }); + } +} + +interface KesadaranResult { + /** kode sadar */ + kdSadar: string; + + /** nama sadar */ + nmSadar: string; +} From e46b5c368654aac0bd752240ef826a1752b9be4a Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:28:30 +0800 Subject: [PATCH 10/29] feat(test): add test for pcare get data kesadaran --- test/pcare/kesadaran.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/pcare/kesadaran.test.ts diff --git a/test/pcare/kesadaran.test.ts b/test/pcare/kesadaran.test.ts new file mode 100644 index 0000000..2e5e989 --- /dev/null +++ b/test/pcare/kesadaran.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Kesadaran', { timeout: 25_000 }, () => { + it.concurrent('dataKesadaran()', async () => { + const result = await jkn.pcare.kesadaran.get(); + expect(result.metaData.code).toBe(200); // no example data + }); +}); From 6143ae9a5efedb5a6f01d381b52f53571ab3ae48 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:45:01 +0800 Subject: [PATCH 11/29] feat: add pcare get data poli FKTP --- src/pcare/index.ts | 5 +++++ src/pcare/poli.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/pcare/poli.ts diff --git a/src/pcare/index.ts b/src/pcare/index.ts index ad7b948..f0f2870 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -2,6 +2,7 @@ import { CachedApi } from '../base.js'; import { Diagnosa } from './diagnosa.js'; import { Dokter } from './dokter.js'; import { Kesadaran } from './kesadaran.js'; +import { Poli } from './poli.js'; export class PCare { private static instance: PCare | undefined; @@ -26,4 +27,8 @@ export class PCare { get kesadaran() { return this.cache.get('pcare_kesadaran', Kesadaran); } + + get poli() { + return this.cache.get('pcare_poli_fktp', Poli); + } } diff --git a/src/pcare/poli.ts b/src/pcare/poli.ts new file mode 100644 index 0000000..63f1346 --- /dev/null +++ b/src/pcare/poli.ts @@ -0,0 +1,26 @@ +import { PCareBaseApi } from './base.js'; + +export class Poli extends PCareBaseApi { + /** + * Get Data Poli FKTP + * @param params params{row, limit} + */ + async get(params: { row: number; limit: number }) { + return this.send<{ count: number; list: PoliResult[] }>({ + name: `${this.name} Data Poli FKTP`, + path: ['/poli/fktp/:row/:limit', params], + method: 'GET' + }); + } +} + +interface PoliResult { + /** kode poliklinik */ + kdPoli: string; + + /** nama polikinink */ + nmPoli: string; + + /** poli sakit */ + poliSakit: boolean; +} From 6db328eca90df0c6ef65a0ea7d2c77b60e8913f3 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Tue, 24 Mar 2026 23:45:21 +0800 Subject: [PATCH 12/29] feat(test): add test pcare get data poli FKTP --- test/pcare/poli.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/pcare/poli.test.ts diff --git a/test/pcare/poli.test.ts b/test/pcare/poli.test.ts new file mode 100644 index 0000000..b286ee7 --- /dev/null +++ b/test/pcare/poli.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Poli FKTP', { timeout: 25_000 }, () => { + it.concurrent('dataPoliFKTP()', async () => { + const result = await jkn.pcare.poli.get({ row: 1, limit: 10 }); + expect(result.metaData.code).toBe(200); // no example data + }); +}); From dc5dd2ab48c5bf96e25295c4d2ff4a54b0c02c83 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 00:02:08 +0800 Subject: [PATCH 13/29] feat: move all pacare refensi to file referensi --- src/pcare/diagnosa.ts | 35 ------------ src/pcare/dokter.ts | 29 ---------- src/pcare/index.ts | 21 +------ src/pcare/kesadaran.ts | 22 -------- src/pcare/poli.ts | 26 --------- src/pcare/referensi.ts | 103 +++++++++++++++++++++++++++++++++++ test/pcare/diagnosa.test.ts | 9 --- test/pcare/dokter.test.ts | 9 --- test/pcare/kesadaran.test.ts | 9 --- test/pcare/poli.test.ts | 9 --- test/pcare/referensi.test.ts | 24 ++++++++ 11 files changed, 130 insertions(+), 166 deletions(-) delete mode 100644 src/pcare/diagnosa.ts delete mode 100644 src/pcare/dokter.ts delete mode 100644 src/pcare/kesadaran.ts delete mode 100644 src/pcare/poli.ts create mode 100644 src/pcare/referensi.ts delete mode 100644 test/pcare/diagnosa.test.ts delete mode 100644 test/pcare/dokter.test.ts delete mode 100644 test/pcare/kesadaran.test.ts delete mode 100644 test/pcare/poli.test.ts create mode 100644 test/pcare/referensi.test.ts diff --git a/src/pcare/diagnosa.ts b/src/pcare/diagnosa.ts deleted file mode 100644 index 7494baa..0000000 --- a/src/pcare/diagnosa.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PCareBaseApi } from './base.js'; - -export class Diagnosa extends PCareBaseApi { - /** - * Get Data Diagnosa - * @param params param {kode, row, limit} - */ - async get(params: { - /** Kode atau nama diagnosa */ - kode: string; - - /** Row data awal yang akan ditampilkan */ - row: number; - - /** Limit jumlah data yang akan ditampilkan */ - limit: number; - }) { - return this.send<{ count: number; list: DiagnosaResult[] }>({ - name: `${this.name} Data Diagnosa`, - path: ['/diagnosa/:kode/:row/:limit', params], - method: 'GET' - }); - } -} - -interface DiagnosaResult { - /** kode diagnosa */ - kdDiag: string; - - /** nama diagnonsa */ - nmDiag: string; - - /** non-spesialis */ - nonSpesialis: boolean; -} diff --git a/src/pcare/dokter.ts b/src/pcare/dokter.ts deleted file mode 100644 index 7dce380..0000000 --- a/src/pcare/dokter.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { PCareBaseApi } from './base.js'; - -export class Dokter extends PCareBaseApi { - /** - * Get Data Dokter - * @param params params{row,limit} - */ - async get(params: { - /** Row data awal yang akan ditampilkan */ - row: number; - - /** Limit jumlah data yang akan ditampilkan */ - limit: number; - }) { - return this.send<{ count: number; list: DokterResult[] }>({ - name: `${this.name} Data Dokter`, - path: ['/dokter/:row/:limit', params], - method: 'GET' - }); - } -} - -interface DokterResult { - /** kode dokter */ - kdDokter: string; - - /** nama dokter */ - nmDokter: string; -} diff --git a/src/pcare/index.ts b/src/pcare/index.ts index f0f2870..8ba52c6 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -1,8 +1,5 @@ import { CachedApi } from '../base.js'; -import { Diagnosa } from './diagnosa.js'; -import { Dokter } from './dokter.js'; -import { Kesadaran } from './kesadaran.js'; -import { Poli } from './poli.js'; +import { Referensi } from './referensi.js'; export class PCare { private static instance: PCare | undefined; @@ -16,19 +13,7 @@ export class PCare { return this.instance; } - get diagnosa() { - return this.cache.get('pcare_diagnosa', Diagnosa); - } - - get dokter() { - return this.cache.get('pcare_dokter', Dokter); - } - - get kesadaran() { - return this.cache.get('pcare_kesadaran', Kesadaran); - } - - get poli() { - return this.cache.get('pcare_poli_fktp', Poli); + get referensi() { + return this.cache.get('pcare_referensi', Referensi); } } diff --git a/src/pcare/kesadaran.ts b/src/pcare/kesadaran.ts deleted file mode 100644 index d43aba4..0000000 --- a/src/pcare/kesadaran.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PCareBaseApi } from './base.js'; - -export class Kesadaran extends PCareBaseApi { - /** - * Get Data Kesadaran - */ - async get() { - return this.send<{ count: number; list: KesadaranResult[] }>({ - name: `${this.name} Data Kesadaran`, - path: '/kesadaran', - method: 'GET' - }); - } -} - -interface KesadaranResult { - /** kode sadar */ - kdSadar: string; - - /** nama sadar */ - nmSadar: string; -} diff --git a/src/pcare/poli.ts b/src/pcare/poli.ts deleted file mode 100644 index 63f1346..0000000 --- a/src/pcare/poli.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PCareBaseApi } from './base.js'; - -export class Poli extends PCareBaseApi { - /** - * Get Data Poli FKTP - * @param params params{row, limit} - */ - async get(params: { row: number; limit: number }) { - return this.send<{ count: number; list: PoliResult[] }>({ - name: `${this.name} Data Poli FKTP`, - path: ['/poli/fktp/:row/:limit', params], - method: 'GET' - }); - } -} - -interface PoliResult { - /** kode poliklinik */ - kdPoli: string; - - /** nama polikinink */ - nmPoli: string; - - /** poli sakit */ - poliSakit: boolean; -} diff --git a/src/pcare/referensi.ts b/src/pcare/referensi.ts new file mode 100644 index 0000000..272e0f9 --- /dev/null +++ b/src/pcare/referensi.ts @@ -0,0 +1,103 @@ +import { PCareBaseApi } from './base.js'; + +export class Referensi extends PCareBaseApi { + /** + * Get Data Diagnosa + * @param params param {kode, row, limit} + */ + async diagnosa(params: { + /** Kode atau nama diagnosa */ + kode: string; + + /** Row data awal yang akan ditampilkan */ + row: number; + + /** Limit jumlah data yang akan ditampilkan */ + limit: number; + }) { + return this.send<{ count: number; list: DiagnosaResult[] }>({ + name: `${this.name} Data Diagnosa`, + path: ['/diagnosa/:kode/:row/:limit', params], + method: 'GET' + }); + } + + /** + * Get Data Dokter + * @param params params{row,limit} + */ + async dokter(params: { + /** Row data awal yang akan ditampilkan */ + row: number; + + /** Limit jumlah data yang akan ditampilkan */ + limit: number; + }) { + return this.send<{ count: number; list: DokterResult[] }>({ + name: `${this.name} Data Dokter`, + path: ['/dokter/:row/:limit', params], + method: 'GET' + }); + } + + /** + * Get Data Kesadaran + */ + async kesadaran() { + return this.send<{ count: number; list: KesadaranResult[] }>({ + name: `${this.name} Data Kesadaran`, + path: '/kesadaran', + method: 'GET' + }); + } + + /** + * Get Data Poli FKTP + * @param params params{row, limit} + */ + async poli(params: { row: number; limit: number }) { + return this.send<{ count: number; list: PoliResult[] }>({ + name: `${this.name} Data Poli FKTP`, + path: ['/poli/fktp/:row/:limit', params], + method: 'GET' + }); + } +} + +interface PoliResult { + /** kode poliklinik */ + kdPoli: string; + + /** nama polikinink */ + nmPoli: string; + + /** poli sakit */ + poliSakit: boolean; +} + +interface KesadaranResult { + /** kode sadar */ + kdSadar: string; + + /** nama sadar */ + nmSadar: string; +} + +interface DokterResult { + /** kode dokter */ + kdDokter: string; + + /** nama dokter */ + nmDokter: string; +} + +interface DiagnosaResult { + /** kode diagnosa */ + kdDiag: string; + + /** nama diagnonsa */ + nmDiag: string; + + /** non-spesialis */ + nonSpesialis: boolean; +} diff --git a/test/pcare/diagnosa.test.ts b/test/pcare/diagnosa.test.ts deleted file mode 100644 index 4334f20..0000000 --- a/test/pcare/diagnosa.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import jkn from '../jkn'; - -describe('PCare - Diagnnosa', { timeout: 25_000 }, () => { - it.concurrent('dataDiagnosa()', async () => { - const result = await jkn.pcare.diagnosa.get({ kode: 'A00', row: 1, limit: 10 }); - expect(result.metaData.code).toBe(200); // no example data - }); -}); diff --git a/test/pcare/dokter.test.ts b/test/pcare/dokter.test.ts deleted file mode 100644 index 890ccea..0000000 --- a/test/pcare/dokter.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import jkn from '../jkn'; - -describe('PCare - Dokter', { timeout: 25_000 }, () => { - it.concurrent('dataDokter()', async () => { - const result = await jkn.pcare.dokter.get({ row: 1, limit: 10 }); - expect(result.metaData.code).toBe(200); // no example data - }); -}); diff --git a/test/pcare/kesadaran.test.ts b/test/pcare/kesadaran.test.ts deleted file mode 100644 index 2e5e989..0000000 --- a/test/pcare/kesadaran.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import jkn from '../jkn'; - -describe('PCare - Kesadaran', { timeout: 25_000 }, () => { - it.concurrent('dataKesadaran()', async () => { - const result = await jkn.pcare.kesadaran.get(); - expect(result.metaData.code).toBe(200); // no example data - }); -}); diff --git a/test/pcare/poli.test.ts b/test/pcare/poli.test.ts deleted file mode 100644 index b286ee7..0000000 --- a/test/pcare/poli.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import jkn from '../jkn'; - -describe('PCare - Poli FKTP', { timeout: 25_000 }, () => { - it.concurrent('dataPoliFKTP()', async () => { - const result = await jkn.pcare.poli.get({ row: 1, limit: 10 }); - expect(result.metaData.code).toBe(200); // no example data - }); -}); diff --git a/test/pcare/referensi.test.ts b/test/pcare/referensi.test.ts new file mode 100644 index 0000000..d4cb82f --- /dev/null +++ b/test/pcare/referensi.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Referensi', { timeout: 25_000 }, () => { + it.concurrent('dataDiagnosa() - 200 ok', async () => { + const result = await jkn.pcare.referensi.diagnosa({ kode: 'A00', row: 1, limit: 10 }); + expect(result.metaData.code).toBe(200); + }); + + it.concurrent('dokter() - 200 ok', async () => { + const result = await jkn.pcare.referensi.dokter({ row: 1, limit: 10 }); + expect(result.metaData.code).toBe(200); + }); + + it.concurrent('kesadaran() - 200 ok', async () => { + const result = await jkn.pcare.referensi.kesadaran(); + expect(result.metaData.code).toBe(200); + }); + + it.concurrent('poli() - 200 ok', async () => { + const result = await jkn.pcare.referensi.poli({ row: 1, limit: 10 }); + expect(result.metaData.code).toBe(200); + }); +}); From 539a20f3ba56d924e9413d85eae63dd81793cd31 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 00:11:25 +0800 Subject: [PATCH 14/29] feat: add pcare referensi for get data alergi --- src/pcare/referensi.ts | 23 +++++++++++++++++++++++ test/pcare/referensi.test.ts | 8 +++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/pcare/referensi.ts b/src/pcare/referensi.ts index 272e0f9..5aa90be 100644 --- a/src/pcare/referensi.ts +++ b/src/pcare/referensi.ts @@ -1,6 +1,21 @@ import { PCareBaseApi } from './base.js'; export class Referensi extends PCareBaseApi { + /** + * Get Data Alergi + * @param params param {kode = [01 = Makanan, 02 = Udara, 03 = Obat]} + */ + async alergi(params: { + /** Kode Jenis Alergi [01 = Makanan, 02 = Udara, 03 = Obat] */ + kode: string; + }) { + return this.send<{ count: number; list: AlergiResult[] }>({ + name: `${this.name} Data Alergi`, + path: ['/alergi/jenis/:kode', params], + method: 'GET' + }); + } + /** * Get Data Diagnosa * @param params param {kode, row, limit} @@ -101,3 +116,11 @@ interface DiagnosaResult { /** non-spesialis */ nonSpesialis: boolean; } + +interface AlergiResult { + /** kode alergi */ + kdAlergi: string; + + /** nama alergi */ + nmAlergi: string; +} diff --git a/test/pcare/referensi.test.ts b/test/pcare/referensi.test.ts index d4cb82f..dce8b05 100644 --- a/test/pcare/referensi.test.ts +++ b/test/pcare/referensi.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import jkn from '../jkn'; describe('PCare - Referensi', { timeout: 25_000 }, () => { - it.concurrent('dataDiagnosa() - 200 ok', async () => { + it.concurrent('diagnosa() - 200 ok', async () => { const result = await jkn.pcare.referensi.diagnosa({ kode: 'A00', row: 1, limit: 10 }); expect(result.metaData.code).toBe(200); }); @@ -21,4 +21,10 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { const result = await jkn.pcare.referensi.poli({ row: 1, limit: 10 }); expect(result.metaData.code).toBe(200); }); + + it.concurrent('alergi() - 200 ok', async () => { + const result = await jkn.pcare.referensi.alergi({ kode: '01' }); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.nmAlergi)).toContain('Seafood'); + }); }); From 376d42f24b1543e60601e5978554eaf294284c20 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 00:51:37 +0800 Subject: [PATCH 15/29] feat: add pcare for referensi data prognosa --- src/pcare/referensi.ts | 19 +++++++++++++++++++ test/pcare/referensi.test.ts | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/src/pcare/referensi.ts b/src/pcare/referensi.ts index 5aa90be..56d56ff 100644 --- a/src/pcare/referensi.ts +++ b/src/pcare/referensi.ts @@ -77,6 +77,17 @@ export class Referensi extends PCareBaseApi { method: 'GET' }); } + + /** + * Get Data Prognosa + */ + async prognosa() { + return this.send<{ count: number; list: PrognosaResult[] }>({ + name: `${this.name} Data Prognosa`, + path: '/prognosa', + method: 'GET' + }); + } } interface PoliResult { @@ -124,3 +135,11 @@ interface AlergiResult { /** nama alergi */ nmAlergi: string; } + +interface PrognosaResult { + /** kode prognosa */ + kdPrognosa: string; + + /** nama prognosa */ + nmPrognosa: string; +} diff --git a/test/pcare/referensi.test.ts b/test/pcare/referensi.test.ts index dce8b05..2a48684 100644 --- a/test/pcare/referensi.test.ts +++ b/test/pcare/referensi.test.ts @@ -27,4 +27,10 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { expect(result.metaData.code).toBe(200); expect(result.response?.list.map((d) => d.nmAlergi)).toContain('Seafood'); }); + + it.concurrent('prognosa() - 200 ok', async () => { + const result = await jkn.pcare.referensi.prognosa(); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.kdPrognosa)).toContain('01'); + }); }); From 740639ec83109e538464208a60e04a570613d80c Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 00:51:58 +0800 Subject: [PATCH 16/29] feat: add pcare for get data peserta --- src/pcare/index.ts | 5 +++ src/pcare/peserta.ts | 73 ++++++++++++++++++++++++++++++++++++++ test/pcare/peserta.test.ts | 53 +++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/pcare/peserta.ts create mode 100644 test/pcare/peserta.test.ts diff --git a/src/pcare/index.ts b/src/pcare/index.ts index 8ba52c6..3a78fa9 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -1,4 +1,5 @@ import { CachedApi } from '../base.js'; +import { Peserta } from './peserta.js'; import { Referensi } from './referensi.js'; export class PCare { @@ -16,4 +17,8 @@ export class PCare { get referensi() { return this.cache.get('pcare_referensi', Referensi); } + + get peserta() { + return this.cache.get('pcare_peserta', Peserta); + } } diff --git a/src/pcare/peserta.ts b/src/pcare/peserta.ts new file mode 100644 index 0000000..92ce95e --- /dev/null +++ b/src/pcare/peserta.ts @@ -0,0 +1,73 @@ +import { PCareBaseApi } from './base.js'; + +export class Peserta extends PCareBaseApi { + /** + * Pencarian data peserta berdasarkan nomor kartu + */ + async nomorKartu(params: { + /** nomor kartu JKN atau BPJS */ + nomor: string; + }) { + return this.send({ + name: this.name + 'No. Kartu BPJS', + path: ['/peserta/:nomor', params], + method: 'GET' + }); + } + + /** + * Pencarian data peserta berdasarkan Jenis Kartu + */ + async jenisKartu(params: { + /** jenis kartu [nomor induk kependudukan = nik, nomor kartu BPJS = noka]*/ + jenis: string; + /** nomor NIK/NOKA BPJS */ + nomor: string; + }) { + return this.send({ + name: this.name + 'Peserta by Jenis Kartu', + path: ['/peserta/:jenis/:nomor', params], + method: 'GET' + }); + } +} + +interface DataPeserta { + noKartu: string; + nama: string; + hubunganKeluarga: string; + sex: string; + tglLahir: string; + tglMulaiAktif: string; + tglAkhirBerlaku: string; + kdProviderPst: { + kdProvider: string; + nmProvider: string; + }; + kdProviderGigi: { + kdProvider: string | null; + nmProvider: string | null; + }; + jnsKelas: { + nama: string; + kode: string; + }; + jnsPeserta: { + nama: string; + kode: string; + }; + golDarah: string; + noHP: string; + noKTP: string; + pstProl: string; + pstPrb: string; + aktif: boolean; + ketAktif: string; + asuransi: { + kdAsuransi: string | null; + nmAsuransi: string | null; + noAsuransi: string | null; + cob: boolean; + }; + tunggakan: number; +} diff --git a/test/pcare/peserta.test.ts b/test/pcare/peserta.test.ts new file mode 100644 index 0000000..5eeb005 --- /dev/null +++ b/test/pcare/peserta.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Peserta', { timeout: 25_000 }, () => { + it.concurrent('nomorKartu() - 204 NO_CONTENT', async () => { + const result = await jkn.pcare.peserta.nomorKartu({ + nomor: '0000000000000' + }); + expect(result.metaData.code).toBe(204); + }); + it.concurrent('nomorKartu() - 200 ok', async () => { + const result = await jkn.pcare.peserta.nomorKartu({ + nomor: '0002060616655' + }); + expect(result.metaData.code).toBe(200); + expect(result.response?.noKartu).toBe('0002060616655'); + }); + + it.concurrent('jenisKartu(noka, nomor) - 204 NO_CONTENT', async () => { + const result = await jkn.pcare.peserta.jenisKartu({ + jenis: 'noka', + nomor: '0000000000000' + }); + expect(result.metaData.code).toBe(204); + }); + + it.concurrent('jenisKartu(noka, nomor) - 200 ok', async () => { + const result = await jkn.pcare.peserta.jenisKartu({ + jenis: 'noka', + nomor: '0002060616655' + }); + expect(result.metaData.code).toBe(200); + expect(result.response?.noKartu).toBe('0002060616655'); + }); + + it.concurrent('jenisKartu(nik, nomor) - 500 wrong final block length', async () => { + const result = await jkn.pcare.peserta.jenisKartu({ + jenis: 'nik', + nomor: '000000000000000' + }); + console.log(result); + expect(result.metaData.code).toBe('500'); + }); + + it.concurrent('jenisKartu(nik, nomor) - 200 ok', async () => { + const result = await jkn.pcare.peserta.jenisKartu({ + jenis: 'nik', + nomor: '1705065003000004' + }); + expect(result.metaData.code).toBe(200); + expect(result.response?.noKartu).toBe('0002060616655'); + }); +}); From cef0fbd58ebef2cc2747fc4ce301f63790c14b83 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 01:03:28 +0800 Subject: [PATCH 17/29] feat: add pcare for referensi status pulang --- src/pcare/referensi.ts | 22 ++++++++++++++++++++++ test/pcare/referensi.test.ts | 12 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/pcare/referensi.ts b/src/pcare/referensi.ts index 56d56ff..0ba7f38 100644 --- a/src/pcare/referensi.ts +++ b/src/pcare/referensi.ts @@ -88,6 +88,20 @@ export class Referensi extends PCareBaseApi { method: 'GET' }); } + + /** + * Get Status Pulang + */ + async statuspulang(params: { + /** status rawat inap true or false */ + ranap: boolean; + }) { + return this.send<{ count: number; list: StatusPulangResult[] }>({ + name: `${this.name} Status Pulang`, + path: `/statuspulang/rawatInap/${params.ranap}`, + method: 'GET' + }); + } } interface PoliResult { @@ -143,3 +157,11 @@ interface PrognosaResult { /** nama prognosa */ nmPrognosa: string; } + +interface StatusPulangResult { + /** kode status pulang */ + kdStatusPulang: string; + + /** nama status pulang */ + nmStatusPulang: string; +} diff --git a/test/pcare/referensi.test.ts b/test/pcare/referensi.test.ts index 2a48684..778fe69 100644 --- a/test/pcare/referensi.test.ts +++ b/test/pcare/referensi.test.ts @@ -33,4 +33,16 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { expect(result.metaData.code).toBe(200); expect(result.response?.list.map((d) => d.kdPrognosa)).toContain('01'); }); + + it.concurrent('satatuspulang(ranap = true) - 200 ok', async () => { + const result = await jkn.pcare.referensi.statuspulang({ ranap: true }); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.nmStatusPulang)).toContain('Sembuh'); + }); + + it.concurrent('satatuspulang(ranap = false) - 200 ok', async () => { + const result = await jkn.pcare.referensi.statuspulang({ ranap: false }); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.nmStatusPulang)).toContain('Berobat Jalan'); + }); }); From 0cd607632a001ec3afd9e2d6b4a395cf394d6c31 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 01:41:53 +0800 Subject: [PATCH 18/29] feat: add pcare referrensi for get data provider --- src/pcare/referensi.ts | 26 ++++++++++++++++++++++++++ test/pcare/referensi.test.ts | 5 +++++ 2 files changed, 31 insertions(+) diff --git a/src/pcare/referensi.ts b/src/pcare/referensi.ts index 0ba7f38..f3dda80 100644 --- a/src/pcare/referensi.ts +++ b/src/pcare/referensi.ts @@ -102,6 +102,24 @@ export class Referensi extends PCareBaseApi { method: 'GET' }); } + + /** + * Get Data Provider Rayonisasi + * @param params params{row, limit} + */ + async provider(params: { + /** Row data awal yang akan ditampilkan */ + row: number; + + /** Limit jumlah data yang akan ditampilkan */ + limit: number; + }) { + return this.send<{ count: number; list: ProviderResult[] }>({ + name: `${this.name} Data Provider Rayonisasi`, + path: ['/provider/:row/:limit', params], + method: 'GET' + }); + } } interface PoliResult { @@ -165,3 +183,11 @@ interface StatusPulangResult { /** nama status pulang */ nmStatusPulang: string; } + +interface ProviderResult { + /** kode provider */ + kdProvider: string; + + /** nama provider */ + nmProvider: string; +} diff --git a/test/pcare/referensi.test.ts b/test/pcare/referensi.test.ts index 778fe69..18e21d1 100644 --- a/test/pcare/referensi.test.ts +++ b/test/pcare/referensi.test.ts @@ -45,4 +45,9 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { expect(result.metaData.code).toBe(200); expect(result.response?.list.map((d) => d.nmStatusPulang)).toContain('Berobat Jalan'); }); + + it.concurrent('provider() - 200 ok', async () => { + const result = await jkn.pcare.referensi.provider({ row: 1, limit: 10 }); + expect(result.metaData.code).toBe(200); + }); }); From dcfc8f601355a4547787ffa26807d2550ff4ac9e Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 01:42:37 +0800 Subject: [PATCH 19/29] feat: add pcare for data kunjungan --- src/pcare/index.ts | 5 + src/pcare/kunjungan.ts | 183 +++++++++++++++++++++++++++++++++++ test/pcare/kunjungan.test.ts | 19 ++++ 3 files changed, 207 insertions(+) create mode 100644 src/pcare/kunjungan.ts create mode 100644 test/pcare/kunjungan.test.ts diff --git a/src/pcare/index.ts b/src/pcare/index.ts index 3a78fa9..6955eb8 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -1,4 +1,5 @@ import { CachedApi } from '../base.js'; +import { Kunjungan } from './kunjungan.js'; import { Peserta } from './peserta.js'; import { Referensi } from './referensi.js'; @@ -21,4 +22,8 @@ export class PCare { get peserta() { return this.cache.get('pcare_peserta', Peserta); } + + get kunjungan() { + return this.cache.get('pcare_kunjungan', Kunjungan); + } } diff --git a/src/pcare/kunjungan.ts b/src/pcare/kunjungan.ts new file mode 100644 index 0000000..aca13da --- /dev/null +++ b/src/pcare/kunjungan.ts @@ -0,0 +1,183 @@ +import { PCareBaseApi } from './base.js'; + +export class Kunjungan extends PCareBaseApi { + /** + * Get Data Rujukan + * @param params param { nomor = Nomor Rujukan } + */ + async rujukan(params: { + /** Nomor Rujukan */ + nomor: string; + }) { + return this.send({ + name: `${this.name} Data Rujukan`, + path: ['/kunjungan/rujukan/:nomor', params], + method: 'GET' + }); + } + + /** + * Get Data Riwayat Kunjungan + * @param params param { nomor = Nomor Rujukan } + */ + async riwayat(params: { + /** Nomor Kartu Peserta */ + nomor: string; + }) { + return this.send<{ count: number; list: RiwayatKunjunganResult[] }>({ + name: `${this.name} Data Riwayat Kunjungan`, + path: ['/kunjungan/peserta/:nomor', params], + method: 'GET' + }); + } +} + +interface RujukanResult { + noRujukan: string; + ppk: { + kdPPK: string; + nmPPK: string; + alamat: string | null; + kc: { + kdKC: string; + nmKC: string; + alamat: string | null; + telp: string | null; + fax: string | null; + dati: { + kdProp: string | null; + kdDati: string; + nmDati: string; + }; + kdKR: { + kdKR: string; + nmKR: string; + alamat: string | null; + telp: string | null; + fax: string | null; + }; + }; + }; + tglKunjungan: string; + poli: { + kdPoli: string; + nmPoli: string; + }; + nokaPst: string; + nmPst: string; + tglLahir: string; + pisa: string; + ketPisa: string; + sex: string; + diag1: { + kdDiag: string; + nmDiag: string; + }; + diag2: string | null; + diag3: string | null; + catatan: string; + dokter: { + kdDokter: string; + nmDokter: string; + }; + tacc: { + nmTacc: string | null; + alasanTacc: string | null; + }; + infoDenda: string; +} + +interface RiwayatKunjunganResult { + noKunjungan: string; + tglKunjungan: string; + providerPelayanan: { + kdProvider: string; + nmProvider: string; + }; + peserta: { + noKartu: string; + nama: string | null; + hubunganKeluarga: string; + sex: string | null; + tglLahir: string | null; + tglMulaiAktif: string | null; + tglAkhirBerlaku: string | null; + kdPpkPst: string | null; + kdPpkGigi: string | null; + jnsKelas: string | null; + jnsPeserta: string | null; + golDarah: string | null; + noHP: string | null; + noKTP: string | null; + asuransi: string | null; + }; + poli: { + kdPoli: string; + nmPoli: string; + poliSakit: boolean; + }; + progProlanis: { + kdProgram: string; + nmProgram: string; + }; + keluhan: string; + diagnosa1: { + kdDiag: string; + nmDiag: string; + nonSpesialis: boolean; + }; + diagnosa2: { + kdDiag: string | null; + nmDiag: string | null; + nonSpesialis: boolean; + }; + diagnosa3: { + kdDiag: string | null; + nmDiag: string | null; + nonSpesialis: boolean; + }; + kesadaran: { + kdSadar: string; + nmSadar: string; + }; + sistole: number; + diastole: number; + beratBadan: number; + tinggiBadan: number; + respRate: number; + heartRate: number; + catatan: string; + rujukBalik: number; + providerAsalRujuk: { + kdProvider: string; + nmProvider: string | null; + }; + providerRujukLanjut: { + kdProvider: string; + nmProvider: string; + }; + pemFisikLain: string; + dokter: { + kdDokter: string; + nmDokter: string; + }; + statusPulang: { + kdStatusPulang: string; + nmStatusPulang: string; + }; + tkp: { + kdTkp: string; + nmTkp: string; + }; + poliRujukInternal: { + kdPoli: string | null; + nmPoli: string | null; + poliSakit: boolean; + }; + poliRujukLanjut: { + kdPoli: string; + nmPoli: string; + poliSakit: boolean; + }; + tglPulang: string; +} diff --git a/test/pcare/kunjungan.test.ts b/test/pcare/kunjungan.test.ts new file mode 100644 index 0000000..161bd7c --- /dev/null +++ b/test/pcare/kunjungan.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Kunjungan', { timeout: 25_000 }, () => { + it.concurrent('rujukan() - 500 wrong final block length', async () => { + const result = await jkn.pcare.kunjungan.rujukan({ nomor: '0114A0260815Y000003' }); + expect(result.metaData.code).toBe('500'); + }); + it.concurrent('rujukan() - 200 ok', async () => { + const result = await jkn.pcare.kunjungan.rujukan({ nomor: '0108B0110326Y000002' }); + expect(result.metaData.code).toBe(200); + }); + + it.concurrent('riwayat() - 200 ok', async () => { + const result = await jkn.pcare.kunjungan.riwayat({ nomor: '0002060616655' }); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.peserta.noKartu)).toContain('0002060616655'); + }); +}); From 1278ea0183909ff4eb925c4d76cf276caf299e94 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 01:44:11 +0800 Subject: [PATCH 20/29] feat: add pcare referensi for data spesialis --- src/pcare/referensi.ts | 19 +++++++++++++++++++ test/pcare/referensi.test.ts | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/src/pcare/referensi.ts b/src/pcare/referensi.ts index f3dda80..46a3d12 100644 --- a/src/pcare/referensi.ts +++ b/src/pcare/referensi.ts @@ -120,6 +120,17 @@ export class Referensi extends PCareBaseApi { method: 'GET' }); } + + /** + * Get Data Referensi Spesialis + */ + async spesialis() { + return this.send<{ count: number; list: SpesialisResult[] }>({ + name: `${this.name} Data Provider Rayonisasi`, + path: '/spesialis', + method: 'GET' + }); + } } interface PoliResult { @@ -191,3 +202,11 @@ interface ProviderResult { /** nama provider */ nmProvider: string; } + +interface SpesialisResult { + /** kode spesialis */ + kdSpesialis: string; + + /** nama spesialis */ + nmSpesialis: string; +} diff --git a/test/pcare/referensi.test.ts b/test/pcare/referensi.test.ts index 18e21d1..1e7ada7 100644 --- a/test/pcare/referensi.test.ts +++ b/test/pcare/referensi.test.ts @@ -50,4 +50,9 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { const result = await jkn.pcare.referensi.provider({ row: 1, limit: 10 }); expect(result.metaData.code).toBe(200); }); + + it.concurrent('spesialis() - 200 ok', async () => { + const result = await jkn.pcare.referensi.spesialis(); + expect(result.metaData.code).toBe(200); + }); }); From 65cf466941acd450aa32220f21628f933511e763 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 01:54:17 +0800 Subject: [PATCH 21/29] feat: add pcare referensi for data sub spesialis --- src/pcare/referensi.ts | 29 ++++++++++++++++++++++++++++- test/pcare/referensi.test.ts | 7 +++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/pcare/referensi.ts b/src/pcare/referensi.ts index 46a3d12..3e4608f 100644 --- a/src/pcare/referensi.ts +++ b/src/pcare/referensi.ts @@ -126,11 +126,27 @@ export class Referensi extends PCareBaseApi { */ async spesialis() { return this.send<{ count: number; list: SpesialisResult[] }>({ - name: `${this.name} Data Provider Rayonisasi`, + name: `${this.name} Data Referensi Spesialis`, path: '/spesialis', method: 'GET' }); } + + /** + * Get Data Referensi Sub Spesialis + * @param params params{kode} + * @returns + */ + async subSpesialis(params: { + /** Kode Spesialis */ + kode: string; + }) { + return this.send<{ count: number; list: SubSpesialisResult[] }>({ + name: `${this.name} Data Referensi Sub Spesialis`, + path: ['/spesialis/:kode/subspesialis', params], + method: 'GET' + }); + } } interface PoliResult { @@ -210,3 +226,14 @@ interface SpesialisResult { /** nama spesialis */ nmSpesialis: string; } + +interface SubSpesialisResult { + /** kode sub spesialis */ + kdSubSpesialis: string; + + /** nama sub spesialis */ + nmSubSpesialis: string; + + /** kode poli rujuk */ + kdPoliRujuk: string; +} diff --git a/test/pcare/referensi.test.ts b/test/pcare/referensi.test.ts index 1e7ada7..79434db 100644 --- a/test/pcare/referensi.test.ts +++ b/test/pcare/referensi.test.ts @@ -54,5 +54,12 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { it.concurrent('spesialis() - 200 ok', async () => { const result = await jkn.pcare.referensi.spesialis(); expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.kdSpesialis)).toContain('ANA'); + }); + + it.concurrent('subSpesialis() - 200 ok', async () => { + const result = await jkn.pcare.referensi.subSpesialis({ kode: 'ANA' }); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.kdPoliRujuk)).toContain('ANA'); }); }); From f69a246e8fbe1460c3b07d121110f99a2d0979de Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 02:01:48 +0800 Subject: [PATCH 22/29] fix: move spesialis from referensi file --- src/pcare/index.ts | 5 ++++ src/pcare/referensi.ts | 46 ---------------------------------- src/pcare/spesialis.ts | 48 ++++++++++++++++++++++++++++++++++++ test/pcare/referensi.test.ts | 12 --------- test/pcare/spesialis.test.ts | 16 ++++++++++++ 5 files changed, 69 insertions(+), 58 deletions(-) create mode 100644 src/pcare/spesialis.ts create mode 100644 test/pcare/spesialis.test.ts diff --git a/src/pcare/index.ts b/src/pcare/index.ts index 6955eb8..8c323bc 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -2,6 +2,7 @@ import { CachedApi } from '../base.js'; import { Kunjungan } from './kunjungan.js'; import { Peserta } from './peserta.js'; import { Referensi } from './referensi.js'; +import { Spesialis } from './spesialis.js'; export class PCare { private static instance: PCare | undefined; @@ -26,4 +27,8 @@ export class PCare { get kunjungan() { return this.cache.get('pcare_kunjungan', Kunjungan); } + + get spesialis() { + return this.cache.get('pcare_spesialis', Spesialis); + } } diff --git a/src/pcare/referensi.ts b/src/pcare/referensi.ts index 3e4608f..f3dda80 100644 --- a/src/pcare/referensi.ts +++ b/src/pcare/referensi.ts @@ -120,33 +120,6 @@ export class Referensi extends PCareBaseApi { method: 'GET' }); } - - /** - * Get Data Referensi Spesialis - */ - async spesialis() { - return this.send<{ count: number; list: SpesialisResult[] }>({ - name: `${this.name} Data Referensi Spesialis`, - path: '/spesialis', - method: 'GET' - }); - } - - /** - * Get Data Referensi Sub Spesialis - * @param params params{kode} - * @returns - */ - async subSpesialis(params: { - /** Kode Spesialis */ - kode: string; - }) { - return this.send<{ count: number; list: SubSpesialisResult[] }>({ - name: `${this.name} Data Referensi Sub Spesialis`, - path: ['/spesialis/:kode/subspesialis', params], - method: 'GET' - }); - } } interface PoliResult { @@ -218,22 +191,3 @@ interface ProviderResult { /** nama provider */ nmProvider: string; } - -interface SpesialisResult { - /** kode spesialis */ - kdSpesialis: string; - - /** nama spesialis */ - nmSpesialis: string; -} - -interface SubSpesialisResult { - /** kode sub spesialis */ - kdSubSpesialis: string; - - /** nama sub spesialis */ - nmSubSpesialis: string; - - /** kode poli rujuk */ - kdPoliRujuk: string; -} diff --git a/src/pcare/spesialis.ts b/src/pcare/spesialis.ts new file mode 100644 index 0000000..d3c2565 --- /dev/null +++ b/src/pcare/spesialis.ts @@ -0,0 +1,48 @@ +import { PCareBaseApi } from './base.js'; + +export class Spesialis extends PCareBaseApi { + /** + * Get Data Referensi Spesialis + */ + async get() { + return this.send<{ count: number; list: SpesialisResult[] }>({ + name: `${this.name} Data Referensi Spesialis`, + path: '/spesialis', + method: 'GET' + }); + } + + /** + * Get Data Referensi Sub Spesialis + * @param params params{kode} + */ + async sub(params: { + /** Kode Spesialis */ + kode: string; + }) { + return this.send<{ count: number; list: SubSpesialisResult[] }>({ + name: `${this.name} Data Referensi Sub Spesialis`, + path: ['/spesialis/:kode/subspesialis', params], + method: 'GET' + }); + } +} + +interface SpesialisResult { + /** kode spesialis */ + kdSpesialis: string; + + /** nama spesialis */ + nmSpesialis: string; +} + +interface SubSpesialisResult { + /** kode sub spesialis */ + kdSubSpesialis: string; + + /** nama sub spesialis */ + nmSubSpesialis: string; + + /** kode poli rujuk */ + kdPoliRujuk: string; +} diff --git a/test/pcare/referensi.test.ts b/test/pcare/referensi.test.ts index 79434db..18e21d1 100644 --- a/test/pcare/referensi.test.ts +++ b/test/pcare/referensi.test.ts @@ -50,16 +50,4 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { const result = await jkn.pcare.referensi.provider({ row: 1, limit: 10 }); expect(result.metaData.code).toBe(200); }); - - it.concurrent('spesialis() - 200 ok', async () => { - const result = await jkn.pcare.referensi.spesialis(); - expect(result.metaData.code).toBe(200); - expect(result.response?.list.map((d) => d.kdSpesialis)).toContain('ANA'); - }); - - it.concurrent('subSpesialis() - 200 ok', async () => { - const result = await jkn.pcare.referensi.subSpesialis({ kode: 'ANA' }); - expect(result.metaData.code).toBe(200); - expect(result.response?.list.map((d) => d.kdPoliRujuk)).toContain('ANA'); - }); }); diff --git a/test/pcare/spesialis.test.ts b/test/pcare/spesialis.test.ts new file mode 100644 index 0000000..66979dd --- /dev/null +++ b/test/pcare/spesialis.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Referensi', { timeout: 25_000 }, () => { + it.concurrent('spesialis() - 200 ok', async () => { + const result = await jkn.pcare.spesialis.get(); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.kdSpesialis)).toContain('ANA'); + }); + + it.concurrent('subSpesialis() - 200 ok', async () => { + const result = await jkn.pcare.spesialis.sub({ kode: 'ANA' }); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.kdPoliRujuk)).toContain('ANA'); + }); +}); From 2236ea3aed3e8f989a25a5030694f13b26eda7f5 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 02:05:38 +0800 Subject: [PATCH 23/29] feat: add pcare spesialis for get data sarana --- src/pcare/spesialis.ts | 19 +++++++++++++++++++ test/pcare/spesialis.test.ts | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/src/pcare/spesialis.ts b/src/pcare/spesialis.ts index d3c2565..fadab5c 100644 --- a/src/pcare/spesialis.ts +++ b/src/pcare/spesialis.ts @@ -26,6 +26,17 @@ export class Spesialis extends PCareBaseApi { method: 'GET' }); } + + /** + * Get Data Referensi Sarana + */ + async sarana() { + return this.send<{ count: number; list: SaranaResult[] }>({ + name: `${this.name} Data Referensi Sarana`, + path: '/spesialis/sarana', + method: 'GET' + }); + } } interface SpesialisResult { @@ -46,3 +57,11 @@ interface SubSpesialisResult { /** kode poli rujuk */ kdPoliRujuk: string; } + +interface SaranaResult { + /** kode sarana */ + kdSarana: string; + + /** nama sarana */ + nmSarana: string; +} diff --git a/test/pcare/spesialis.test.ts b/test/pcare/spesialis.test.ts index 66979dd..2d1c79c 100644 --- a/test/pcare/spesialis.test.ts +++ b/test/pcare/spesialis.test.ts @@ -13,4 +13,10 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { expect(result.metaData.code).toBe(200); expect(result.response?.list.map((d) => d.kdPoliRujuk)).toContain('ANA'); }); + + it.concurrent('sarana() - 200 ok', async () => { + const result = await jkn.pcare.spesialis.sarana(); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.nmSarana)).toContain('REKAM MEDIK'); + }); }); From 1b1a7ecf13db3863e0a3b5f683f2ec046cb9a182 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 02:16:45 +0800 Subject: [PATCH 24/29] feat: add pcare spesialis for get data ref khusus --- src/pcare/spesialis.ts | 19 +++++++++++++++++++ test/pcare/spesialis.test.ts | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/src/pcare/spesialis.ts b/src/pcare/spesialis.ts index fadab5c..b9a453c 100644 --- a/src/pcare/spesialis.ts +++ b/src/pcare/spesialis.ts @@ -37,6 +37,17 @@ export class Spesialis extends PCareBaseApi { method: 'GET' }); } + + /** + * Get Data Referensi Khusus + */ + async khusus() { + return this.send<{ count: number; list: KhususResult[] }>({ + name: `${this.name} Data Referensi Khusus`, + path: '/spesialis/khusus', + method: 'GET' + }); + } } interface SpesialisResult { @@ -65,3 +76,11 @@ interface SaranaResult { /** nama sarana */ nmSarana: string; } + +interface KhususResult { + /** kode khusus */ + kdKhusus: string; + + /** nama khusus */ + nmKhusus: string; +} diff --git a/test/pcare/spesialis.test.ts b/test/pcare/spesialis.test.ts index 2d1c79c..d9de4c0 100644 --- a/test/pcare/spesialis.test.ts +++ b/test/pcare/spesialis.test.ts @@ -19,4 +19,10 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { expect(result.metaData.code).toBe(200); expect(result.response?.list.map((d) => d.nmSarana)).toContain('REKAM MEDIK'); }); + + it.concurrent('khusus() - 200 ok', async () => { + const result = await jkn.pcare.spesialis.khusus(); + expect(result.metaData.code).toBe(200); + expect(result.response?.list.map((d) => d.nmKhusus)).toContain('HEMODIALISA'); + }); }); From 55fde4d0043d0863a5ddec8b0efb463cc5c5a7c5 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Wed, 25 Mar 2026 02:31:38 +0800 Subject: [PATCH 25/29] feat: add pcare spesialis for get data faskes rujukan sub spesialis --- src/pcare/spesialis.ts | 37 ++++++++++++++++++++++++++++++++++++ test/pcare/spesialis.test.ts | 9 +++++++++ 2 files changed, 46 insertions(+) diff --git a/src/pcare/spesialis.ts b/src/pcare/spesialis.ts index b9a453c..b08fa5d 100644 --- a/src/pcare/spesialis.ts +++ b/src/pcare/spesialis.ts @@ -48,6 +48,29 @@ export class Spesialis extends PCareBaseApi { method: 'GET' }); } + + /** + * Get Data Faskes Rujukan Sub Spesialis + */ + async faskesRujukanSubSpesialis(params: { + /** Kode Sub Spesialis */ + kodeSub: string; + + /** Kode Sarana */ + kodeSarana: string; + + /** Tanggal Estimasi Rujuk, format: dd-mm-yyyy */ + tanggal: string; + }) { + return this.send<{ count: number; list: FaskesRujukanSubSpesialis[] }>({ + name: `${this.name} Data Faskes Rujukan Sub Spesialis`, + path: [ + '/spesialis/rujuk/subspesialis/:kodeSub/sarana/:kodeSarana/tglEstRujuk/:tanggal', + params + ], + method: 'GET' + }); + } } interface SpesialisResult { @@ -84,3 +107,17 @@ interface KhususResult { /** nama khusus */ nmKhusus: string; } + +interface FaskesRujukanSubSpesialis { + kdppk: string; + nmppk: string; + alamatPpk: string; + telpPpk: string; + kelas: string; + nmkc: string; + distance: number; + jadwal: string; + jmlRujuk: number; + kapasitas: number; + persentase: number; +} diff --git a/test/pcare/spesialis.test.ts b/test/pcare/spesialis.test.ts index d9de4c0..d17c0c2 100644 --- a/test/pcare/spesialis.test.ts +++ b/test/pcare/spesialis.test.ts @@ -25,4 +25,13 @@ describe('PCare - Referensi', { timeout: 25_000 }, () => { expect(result.metaData.code).toBe(200); expect(result.response?.list.map((d) => d.nmKhusus)).toContain('HEMODIALISA'); }); + + it.concurrent('faskesRujukanSubSpesialis() - 200 ok', async () => { + const result = await jkn.pcare.spesialis.faskesRujukanSubSpesialis({ + kodeSub: '26', // kode subSpesialis untuk ANAK + kodeSarana: '1', // kode sarana untuk REKAM MEDIK + tanggal: '25-03-2026' + }); + expect(result.metaData.code).toBe(200); + }); }); From 084611658fe76785dd69504877fe39d016d7c2c7 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Thu, 26 Mar 2026 00:18:23 +0800 Subject: [PATCH 26/29] feat: add pcare kunjungan for insert, update and delete kunjungan --- src/pcare/kunjungan.ts | 162 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/src/pcare/kunjungan.ts b/src/pcare/kunjungan.ts index aca13da..1f665fa 100644 --- a/src/pcare/kunjungan.ts +++ b/src/pcare/kunjungan.ts @@ -30,6 +30,47 @@ export class Kunjungan extends PCareBaseApi { method: 'GET' }); } + + /** + * Add Data Kunjungan + * @param data InputRujukanKhusus | InputRujukanSpesialis + */ + async insert(data: InputKunjunganKhusus | InputKunjunganSpesialis) { + return this.send<{ field: string; messaage: string }>({ + name: `${this.name} Add Data Kunjungan`, + path: '/kunjungan', + method: 'POST', + data: data + }); + } + + /** + * Edit Data Kunjungan + * @param data InputUpdateKunjungan + */ + async update(data: InputUpdateKunjungan) { + return this.send({ + name: `${this.name} Edit Data Kunjungan`, + path: '/kunjungan', + method: 'PUT', + data: data + }); + } + + /** + * Delete Data Kunjungan + * @param params {nomor = Nomor Kunjungan} + */ + async delete(params: { + /** Nomor Kunjungan */ + nomor: string; + }) { + return this.send({ + name: `${this.name} Delete Data Kunjungan`, + path: ['/kunjungan/:nomor', params], + method: 'DELETE' + }); + } } interface RujukanResult { @@ -181,3 +222,124 @@ interface RiwayatKunjunganResult { }; tglPulang: string; } + +interface InputKunjunganKhusus { + noKunjungan: string | null; + noKartu: string; + tglDaftar: string; + kdPoli: string | null; + keluhan: string; + kdSadar: string; + sistole: number; + diastole: number; + beratBadan: number; + tinggiBadan: number; + respRate: number; + heartRate: number; + lingkarPerut: number; + kdStatusPulang: string; + tglPulang: string; + kdDokter: string; + kdDiag1: string; + kdDiag2: string | null; + kdDiag3: string | null; + kdPoliRujukInternal: string | null; + rujukLanjut: { + tglEstRujuk: string; + kdppk: string; + }; + kdTacc: number; + alasanTacc: string | null; + anamnesa: string; + alergiMakan: string; + alergiUdara: string; + alergiObat: string; + kdPrognosa: string; + terapiObat: string; + terapiNonObat: string; + bmhp: string; + suhu: string; +} + +interface InputKunjunganSpesialis { + noKunjungan: string | null; + noKartu: string; + tglDaftar: string; + kdPoli: string; + keluhan: string; + kdSadar: string; + sistole: number; + diastole: number; + beratBadan: number; + tinggiBadan: number; + respRate: number; + heartRate: number; + lingkarPerut: number; + kdStatusPulang: string; + tglPulang: string; + kdDokter: string; + kdDiag1: string; + kdDiag2: string | null; + kdDiag3: string | null; + kdPoliRujukInternal: string | null; + rujukLanjut: { + kdppk: string; + tglEstRujuk: string; + }; + kdTacc: number; + alasanTacc: string | null; + anamnesa: string; + alergiMakan: string; + alergiUdara: string; + alergiObat: string; + kdPrognosa: string; + terapiObat: string; + terapiNonObat: string; + bmhp: string; + suhu: string; +} + +interface InputUpdateKunjungan { + noKunjungan: string; + noKartu: string; + keluhan: string; + kdSadar: string; + sistole: number; + diastole: number; + beratBadan: number; + tinggiBadan: number; + respRate: number; + heartRate: number; + lingkarPerut: number; + kdStatusPulang: string; + tglPulang: string; + kdDokter: string; + kdDiag1: string; + kdDiag2: string | null; + kdDiag3: string | null; + kdPoliRujukInternal: string | null; + rujukLanjut: { + tglEstRujuk: string; + kdppk: string; + subSpesialis: { + kdSubSpesialis1: string; + kdSarana: string | null; + } | null; + khusus: { + kdKhusus: string; + kdSubSpesialis: string | null; + catatan: string; + } | null; + }; + kdTacc: number; + alasanTacc: string; + anamnesa: string; + alergiMakan: string; + alergiUdara: string; + alergiObat: string; + kdPrognosa: string; + terapiObat: string; + terapiNonObat: string; + bmhp: string; + suhu: string; +} From 9012e36037b0797c1902f8ffcc3654f752b4dd2b Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Fri, 27 Mar 2026 00:47:36 +0800 Subject: [PATCH 27/29] fix: update file .env.example --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 937bbcb..f83102d 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ JKN_VCLAIM_USER_KEY= JKN_ANTREAN_USER_KEY= JKN_APOTEK_USER_KEY= JKN_PCARE_USER_KEY= +JKN_PCARE_USER_PASS= JKN_ICARE_USER_KEY= JKN_REKAM_MEDIS_USER_KEY= JKN_APLICARES_USER_KEY= From 75af0714ecfc8431ca8034e97351f7c47a59aaaf Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Fri, 27 Mar 2026 00:48:33 +0800 Subject: [PATCH 28/29] fix(docs): update file README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 608fb73..62a7e8d 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,13 @@ interface Config { * @default process.env.JKN_PCARE_USER_KEY */ pcareUserKey: string; + + /** + * Username dan Password PCare dari BPJS + * + * @default process.env.JKN_PCARE_USER_PASS + */ + pcareUserPass: string; /** * User key i-Care dari BPJS From ebae80eb135a1e17443e3423637cb6449ae66af1 Mon Sep 17 00:00:00 2001 From: Hermanto Lakoro Date: Fri, 27 Mar 2026 00:49:34 +0800 Subject: [PATCH 29/29] fix: update and test partial request PCare api --- src/pcare/index.ts | 17 +++++++++++++++++ test/pcare/partial.test.ts | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 test/pcare/partial.test.ts diff --git a/src/pcare/index.ts b/src/pcare/index.ts index 8c323bc..041434e 100644 --- a/src/pcare/index.ts +++ b/src/pcare/index.ts @@ -1,9 +1,13 @@ import { CachedApi } from '../base.js'; +import { SendOption } from '../fetcher.js'; +import { PCareBaseApi } from './base.js'; import { Kunjungan } from './kunjungan.js'; import { Peserta } from './peserta.js'; import { Referensi } from './referensi.js'; import { Spesialis } from './spesialis.js'; +type RequestOption = SendOption; + export class PCare { private static instance: PCare | undefined; @@ -31,4 +35,17 @@ export class PCare { get spesialis() { return this.cache.get('pcare_spesialis', Spesialis); } + + get api() { + return this.cache.get('pcare_partial', PartialRequest); + } +} + +/** + * Implement base api directly since this is for partial support + */ +class PartialRequest extends PCareBaseApi { + async request(option: RequestOption) { + return this.send(option); + } } diff --git a/test/pcare/partial.test.ts b/test/pcare/partial.test.ts new file mode 100644 index 0000000..5111782 --- /dev/null +++ b/test/pcare/partial.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import jkn from '../jkn'; + +describe('PCare - Partial Request', { timeout: 25_000 }, () => { + interface PCareDiagnosa { + count: number; + list: { + kdDiag: string; + nmDiag: string; + nonSpesialis: boolean; + }[]; + } + + it.concurrent('api() - 200 ok', async () => { + const result = await jkn.pcare.api.request({ + path: `/diagnosa/A00/1/5`, + method: 'GET' + }); + expect(result.metaData.code).toBe(200); + }); +});