diff --git a/.changeset/three-apples-post.md b/.changeset/three-apples-post.md
new file mode 100644
index 0000000..aaa96d9
--- /dev/null
+++ b/.changeset/three-apples-post.md
@@ -0,0 +1,5 @@
+---
+'@ssecd/jkn': minor
+---
+
+Implement Surkon V2 API
diff --git a/README.md b/README.md
index 544dc61..f56d26b 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-JKN (BPJS) Bridging API untuk NodeJS
+# JKN
-
+JKN (BPJS) Bridging API untuk NodeJS
## Fitur
@@ -9,15 +9,26 @@ JKN (BPJS) Bridging API untuk NodeJS
- ✅ Antrean
- ✅ Apotek
- ✅ i-Care
-- ✅ Rekam Medis
+- ✅ Rekam Medis _(Experimental types)_
- 🧩 PCare _([partial](https://github.com/ssecd/jkn/pull/26))_
+## Demo
+
+https://github.com/user-attachments/assets/02809ccd-30ea-48a8-bbb3-1e0df687175a
+
## Instalasi
Instalasi paket dapat dilakukan dengan perintah berikut:
```bash
+# Node
npm install @ssecd/jkn
+
+# Bun
+bun install @ssecd/jkn
+
+# Deno
+deno install npm:@ssecd/jkn
```
Untuk dukungan _type_ pada API Rekam Medis, perlu menambahkan development dependensi `@types/fhir` dengan perintah:
@@ -26,7 +37,7 @@ Untuk dukungan _type_ pada API Rekam Medis, perlu menambahkan development depend
npm install --save-dev @types/fhir
```
-Instalasi juga dapat dilakukan menggunakan `PNPM` atau `YARN`
+> Instalasi paket NodeJS juga dapat dilakukan menggunakan `PNPM` atau `YARN`
## Penggunaan
@@ -94,7 +105,7 @@ console.log(result);
faskes: [
{
kode: "0089S002",
- nama: "Klinik Utama Mata Silampari Sriwijaya Eye Centre"
+ nama: "Silampari Sriwijaya Eye Centre"
}
]
};
@@ -148,7 +159,7 @@ onResponse: ((info: SendOption & { duration: number; type
- `onError`
```ts
-onError: ((error: unknown) => MaybePromise) | undefined = undefined;
+onError: ((error: Error) => MaybePromise) | undefined = undefined;
```
Contoh penggunaan event:
diff --git a/assets/demo.gif b/assets/demo.gif
deleted file mode 100644
index 2ec919a..0000000
Binary files a/assets/demo.gif and /dev/null differ
diff --git a/src/apotek/index.ts b/src/apotek/index.ts
index 6611684..914786e 100644
--- a/src/apotek/index.ts
+++ b/src/apotek/index.ts
@@ -2,6 +2,7 @@ import { CachedApi } from '../base.js';
import { Monitoring } from './monitoring.js';
import { Obat } from './obat.js';
import { PelayananObat } from './pelayanan-obat.js';
+import { PRB } from './prb.js';
import { Referensi } from './referensi.js';
import { Resep } from './resep.js';
import { SEP } from './sep.js';
@@ -41,4 +42,8 @@ export class Apotek {
get monitoring() {
return this.cache.get('apotek_monitoring', Monitoring);
}
+
+ get prb() {
+ return this.cache.get('apotek_prb', PRB);
+ }
}
diff --git a/src/apotek/obat.ts b/src/apotek/obat.ts
index dadc7f5..4ac3ec7 100644
--- a/src/apotek/obat.ts
+++ b/src/apotek/obat.ts
@@ -48,4 +48,17 @@ export class Obat extends ApotekBaseApi {
data
});
}
+
+ /**
+ * Update data stok obat dari sistem informasi apotek ke
+ * aplikasi Apotek Online
+ */
+ async updateStok(data: { KDOBAT: string; STOK: number }) {
+ return this.send({
+ name: this.name + 'Update Stok Obat',
+ path: '/UpdateStokObat/updatestok',
+ method: 'POST',
+ data
+ });
+ }
}
diff --git a/src/apotek/prb.ts b/src/apotek/prb.ts
new file mode 100644
index 0000000..a95d464
--- /dev/null
+++ b/src/apotek/prb.ts
@@ -0,0 +1,32 @@
+import { ApotekBaseApi } from './base.js';
+
+export class PRB extends ApotekBaseApi {
+ /**
+ * Data rekap peserta PRB yang baru dilakukan PRB oleh Faskes
+ */
+ async rekapPeserta(params: {
+ /** tahun */
+ tahun: number;
+
+ /** angka bulan 1 sampai 12 */
+ bulan: number;
+ }) {
+ return this.send<{
+ list: {
+ No: 1;
+ NamaPeserta: string;
+ NomorKaPst: string;
+ Alamat: string;
+ TglSRB: string;
+ Diagnosa: string;
+ Obat: string;
+ DPJP: string;
+ AsalFaskes: string;
+ }[];
+ }>({
+ name: this.name + 'Data Klaim',
+ path: `/Prb/rekappeserta/tahun/${params.tahun}/bulan/${params.bulan}`,
+ method: 'GET'
+ });
+ }
+}
diff --git a/src/apotek/referensi.ts b/src/apotek/referensi.ts
index 338d066..b6b2b94 100644
--- a/src/apotek/referensi.ts
+++ b/src/apotek/referensi.ts
@@ -16,6 +16,8 @@ export class Referensi extends ApotekBaseApi {
restriksi: string;
generik: string;
aktif: string | null;
+ sedia: string;
+ stok: string;
}[];
}>({
name: this.name + 'DPHO',
diff --git a/src/fetcher.ts b/src/fetcher.ts
index 9e556cd..a662314 100644
--- a/src/fetcher.ts
+++ b/src/fetcher.ts
@@ -215,7 +215,7 @@ export class Fetcher {
) => MaybePromise)
| undefined = undefined;
- public onError: ((error: unknown) => MaybePromise) | undefined = undefined;
+ public onError: ((error: Error) => MaybePromise) | undefined = undefined;
private configured = false;
@@ -319,9 +319,9 @@ export class Fetcher {
option: SendOption
): Promise[T]> {
await this.applyConfig();
- if (!option.path.startsWith('/')) throw new Error(`path must be starts with "/"`);
+ if (!option.path.startsWith('/')) throw new Error(`Path must be starts with "/"`);
- let response = '';
+ let result = '';
try {
const baseUrl = this.config.baseUrls[type];
if (!baseUrl) throw new Error(`base url of type "${type}" is invalid`);
@@ -332,7 +332,7 @@ export class Fetcher {
init.headers = headers;
if (option.data) {
- if (option.method === 'GET') throw new Error(`can not pass data with "GET" method`);
+ 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
@@ -350,9 +350,11 @@ export class Fetcher {
this.onRequest?.({ ...option, type });
const startedAt = performance.now();
- response = await fetch(url, init).then((r) => r.text());
- const json: SendResponse[T] = JSON.parse(response);
+ 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));
@@ -362,23 +364,22 @@ export class Fetcher {
this.onResponse?.({ ...option, duration, type }, json);
return json;
} catch (error: unknown) {
- this.onError?.(error);
- if (this.config.throw) {
- if (error instanceof Error) {
- error.message += `. \nResponse: ${response}`;
- }
- throw error;
- }
- let message =
+ const customError = new Error(
error instanceof SyntaxError
- ? 'Received response from the JKN API appears to be in an unexpected format'
- : 'An error occurred while requesting information from the JKN API';
- if (error instanceof Error) message += `. ` + error.message;
- message += '. ' + response;
- console.error(error);
+ ? `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 },
@@ -397,3 +398,19 @@ export class Fetcher {
return this.config;
}
}
+
+/**
+ * A simple HTML parser so ugly HTML error messages
+ * don't hurt your eyes anymore.
+ */
+function parseHtml(html?: string) {
+ if (!html) return '[empty]';
+ return html
+ .replace(/]*>[\s\S]*?<\/head>/gi, '')
+ .replace(/