Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/three-apples-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ssecd/jkn': minor
---

Implement Surkon V2 API
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
JKN (BPJS) Bridging API untuk NodeJS
# JKN

<img width="512" alt="Mudahnya JKN API dari SSEC" src="./assets/demo.gif">
JKN (BPJS) Bridging API untuk NodeJS

## Fitur

Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -94,7 +105,7 @@ console.log(result);
faskes: [
{
kode: "0089S002",
nama: "Klinik Utama Mata Silampari Sriwijaya Eye Centre"
nama: "Silampari Sriwijaya Eye Centre"
}
]
};
Expand Down Expand Up @@ -148,7 +159,7 @@ onResponse: (<T extends Type = Type>(info: SendOption & { duration: number; type
- `onError`

```ts
onError: ((error: unknown) => MaybePromise<void>) | undefined = undefined;
onError: ((error: Error) => MaybePromise<void>) | undefined = undefined;
```

Contoh penggunaan event:
Expand Down
Binary file removed assets/demo.gif
Binary file not shown.
5 changes: 5 additions & 0 deletions src/apotek/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
}
13 changes: 13 additions & 0 deletions src/apotek/obat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<null>({
name: this.name + 'Update Stok Obat',
path: '/UpdateStokObat/updatestok',
method: 'POST',
data
});
}
}
32 changes: 32 additions & 0 deletions src/apotek/prb.ts
Original file line number Diff line number Diff line change
@@ -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'
});
}
}
2 changes: 2 additions & 0 deletions src/apotek/referensi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export class Referensi extends ApotekBaseApi {
restriksi: string;
generik: string;
aktif: string | null;
sedia: string;
stok: string;
}[];
}>({
name: this.name + 'DPHO',
Expand Down
55 changes: 36 additions & 19 deletions src/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export class Fetcher {
) => MaybePromise<void>)
| undefined = undefined;

public onError: ((error: unknown) => MaybePromise<void>) | undefined = undefined;
public onError: ((error: Error) => MaybePromise<void>) | undefined = undefined;

private configured = false;

Expand Down Expand Up @@ -319,9 +319,9 @@ export class Fetcher {
option: SendOption
): Promise<SendResponse<R | undefined, M | undefined>[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`);
Expand All @@ -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
Expand All @@ -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<R, M>[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<R, M>[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));
Expand All @@ -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 },
Expand All @@ -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(/<head\b[^>]*>[\s\S]*?<\/head>/gi, '')
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<[^>]*>/g, '')
.trim()
.replace(/\r?\n+/g, ' - ') // newlines to dash
.replace(/\s+/g, ' '); // normalize whitespace
}
22 changes: 22 additions & 0 deletions src/vclaim/prb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,26 @@ export class PRB extends VClaimBaseApi {
method: 'GET'
});
}

/**
* Menyediakan data rekap klaim yang diajukan oleh Faskes 2, di mana
* dalam klaim tersebut terdapat peserta dengan flagging Potensi PRB.
*/
async rekapPotensi(params: { tahun: number; bulan: number }) {
// TODO: Response returning empty string
return this.send<{
list: {
NamaPeserta: string;
NomorKartu: string;
NOSEP: string;
TanggalPelayanan: string;
DiagnosaPotensiPRB: string;
KategoriPenyakitPRB: string;
}[];
}>({
name: this.name + 'Rekap Potensi PRB',
path: `/prbpotensi/tahun/${params.tahun}/bulan/${params.bulan}`,
method: 'GET'
});
}
}
Loading