Skip to content

Commit

Permalink
feat: Implement Aplicares (#70)
Browse files Browse the repository at this point in the history
* register fetcher

* add aplicares module

* fix aplicares base urls

* extending metadata response props

* add ref kamar

* update readme

* add update bed method

* requiredConfig validation helper

* add doc comments

* add create ruang method

* add read bed method

* fix bed create and update method

* add create and update test

* tidy up read test

* add delete bed method

* don't run test concurrently

* add changeset
  • Loading branch information
mustofa-id authored Dec 23, 2023
1 parent 50eedd6 commit 69f1475
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-donuts-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ssecd/jkn': minor
---

Implement Aplicares services
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ interface Config {
/**
* Kode PPK yang diberikan BPJS.
*
* Diperlukan untuk melakukan proses encryption
* pada web service eRekam Medis.
* Diperlukan untuk melakukan proses enkripsi
* pada service eRekamMedis and request pada
* service Aplicares
*
* @default process.env.JKN_PPK_CODE
*/
Expand All @@ -151,6 +152,13 @@ interface Config {
*/
consSecret: string;

/**
* User key Aplicares dari BPJS
*
* @default process.env.JKN_APLICARES_USER_KEY
*/
aplicaresUserKey: string;

/**
* User key VClaim dari BPJS
*
Expand Down Expand Up @@ -237,6 +245,7 @@ interface Config {

## API Tersedia

- ✅ Aplicares
- ✅ VClaim
- ✅ Antrean
- ✅ Apotek _(experimental)_
Expand Down
139 changes: 139 additions & 0 deletions src/aplicares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { BaseApi } from './base.js';

export class Aplicares extends BaseApi<'aplicares'> {
protected type = 'aplicares' as const;

/**
* Referensi Kamar
*/
async refKamar() {
return this.send<
{
list: {
kodekelas: string;
namakelas: string;
}[];
},
{ totalitems: number }
>({
path: `/rest/ref/kelas`,
method: 'GET',
skipDecrypt: true
});
}

/**
* Update Ketersediaan Tempat Tidur
*
* Property `tersediapria`, `tersediawanita`, dan `tersediapriawanita`
* digunakan untuk faskes yang ingin mencantumkan informasi ketersediaan
* tempat tidur untuk pasien laki-laki, perempuan, dan laki–laki atau
* perempuan.
*/
async update(data: AplicaresBedData) {
const { ppkCode } = await this.requiredConfig('ppkCode');
return this.send({
path: `/rest/bed/update/${ppkCode}`,
method: 'POST',
skipContentTypeHack: true,
headers: { 'Content-Type': 'application/json' },
data
});
}

/**
* Buat Ruangan Baru
*
* Property `tersediapria`, `tersediawanita`, dan `tersediapriawanita`
* digunakan untuk faskes yang ingin mencantumkan informasi ketersediaan
* tempat tidur untuk pasien laki-laki, perempuan, dan laki–laki atau
* perempuan.
*/
async create(data: AplicaresBedData) {
const { ppkCode } = await this.requiredConfig('ppkCode');
return this.send<undefined>({
path: `/rest/bed/create/${ppkCode}`,
method: 'POST',
skipContentTypeHack: true,
headers: { 'Content-Type': 'application/json' },
data
});
}

/**
* Melihat Data Ketersediaan Kamar Faskes
*
* Property `start` dan `limit` berfungsi untuk paging, jika faskes
* ingin menampilkan data dari baris pertama sampai baris kesepuluh
* maka `start` = `1` dan `limit` = `1`, nilai `start` dimulai dari `1`.
*/
async read(params: {
/** paging start */
start: number;

/** paging limit */
limit: number;
}) {
const { ppkCode } = await this.requiredConfig('ppkCode');
return this.send<
{
list: (AplicaresBedData & {
kodeppk: string;
rownumber: number;
lastupdate: string;
})[];
},
{ totalitems: number }
>({
path: `/rest/bed/read/${ppkCode}/${params.start}/${params.limit}`,
method: 'GET',
skipDecrypt: true
});
}

/**
* Hapus Ruangan
*/
async delete(data: {
/** kode kelas ruang rawat sesuai dengan mapping BPJS Kesehatan */
kodekelas: string;

/** kode ruangan faskes */
koderuang: string;
}) {
const { ppkCode } = await this.requiredConfig('ppkCode');
return this.send<undefined>({
path: `/rest/bed/delete/${ppkCode}`,
method: 'POST',
skipContentTypeHack: true,
headers: { 'Content-Type': 'application/json' },
data
});
}
}

interface AplicaresBedData {
/** kode kelas ruang rawat sesuai dengan mapping BPJS Kesehatan */
kodekelas: string;

/** kode ruangan faskes */
koderuang: string;

/** nama ruang rawat faskes */
namaruang: string;

/** kapasitas ruang faskes */
kapasitas: number;

/** jumlah tempat tidur yang kosong atau dapat ditempati pasien baru */
tersedia: number;

/** jumlah tempat tidur yang kosong atau dapat ditempati pasien baru laki – laki */
tersediapria?: number;

/** jumlah tempat tidur yang kosong atau dapat ditempati pasien baru perempuan */
tersediawanita?: number;

/** jumlah tempat tidur yang kosong atau dapat ditempati pasien baru laki – laki atau perempuan */
tersediapriawanita?: number;
}
15 changes: 13 additions & 2 deletions src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@ export abstract class BaseApi<T extends Type = Type> {

constructor(private readonly fetcher: Fetcher) {}

protected send<R>(option: SendOption) {
return this.fetcher.send<T, R>(this.type, option);
protected send<R, M = unknown>(option: SendOption) {
return this.fetcher.send<T, R, M>(this.type, option);
}

protected async getConfig(): Promise<Config> {
return this.fetcher.getConfig();
}

protected async requiredConfig(...keys: (keyof Config)[]) {
const config = await this.getConfig();
for (const key of keys) {
if (!config[key]) {
const message = `The "${key}" config value must not be falsy for this request`;
throw new Error(message);
}
}
return config;
}
}

type CacheKey = `${Type}${string}`;
Expand Down
51 changes: 33 additions & 18 deletions src/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ type MaybePromise<T> = T | Promise<T>;

export type Mode = 'development' | 'production';

export type Type = '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 encryption
* pada web service eRekam Medis.
* Diperlukan untuk melakukan proses enkripsi
* pada service eRekamMedis and request pada
* service Aplicares
*
* @default process.env.JKN_PPK_CODE
*/
Expand All @@ -32,6 +33,13 @@ export interface Config {
*/
consSecret: string;

/**
* User key Aplicares dari BPJS
*
* @default process.env.JKN_APLICARES_USER_KEY
*/
aplicaresUserKey: string;

/**
* User key VClaim dari BPJS
*
Expand Down Expand Up @@ -133,32 +141,37 @@ export interface SendOption {
skipContentTypeHack?: boolean;
}

export interface LowerResponse<T, C = number> {
export interface LowerResponse<T, C, E> {
response: T;
metadata: {
code: C;
message: string;
};
} & E;
}

export interface CamelResponse<T, C = string> {
export interface CamelResponse<T, C, E> {
response: T;
metaData: {
code: C;
message: string;
};
} & E;
}

export type SendResponse<T> = {
antrean: LowerResponse<T>;
vclaim: CamelResponse<T>;
apotek: CamelResponse<T>;
pcare: CamelResponse<T>;
icare: CamelResponse<T, number>;
rekamMedis: LowerResponse<T, string>;
export type SendResponse<T, M> = {
aplicares: LowerResponse<T, number, M>;
antrean: LowerResponse<T, number, M>;
vclaim: CamelResponse<T, string, M>;
apotek: CamelResponse<T, string, M>;
pcare: CamelResponse<T, string, M>;
icare: CamelResponse<T, number, M>;
rekamMedis: LowerResponse<T, string, M>;
};

const defaultBaseUrls: Record<Type, Record<Mode, string>> = {
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'
Expand Down Expand Up @@ -193,6 +206,7 @@ export class Fetcher {
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 ?? '',
Expand Down Expand Up @@ -230,6 +244,7 @@ export class Fetcher {

private get userKeyMap(): Record<Type, string | undefined> {
return {
aplicares: this.config.aplicaresUserKey,
vclaim: this.config.vclaimUserKey,
antrean: this.config.antreanUserKey,
apotek: this.config.apotekUserKey,
Expand Down Expand Up @@ -270,10 +285,10 @@ export class Fetcher {
return lz.decompressFromEncodedURIComponent(text);
}

async send<T extends Type, R>(
async send<T extends Type, R, M>(
type: T,
option: SendOption
): Promise<SendResponse<R | undefined>[T]> {
): Promise<SendResponse<R | undefined, M | undefined>[T]> {
await this.applyConfig();
if (!option.path.startsWith('/')) throw new Error(`path must be starts with "/"`);

Expand Down Expand Up @@ -305,7 +320,7 @@ export class Fetcher {
}

response = await fetch(url, init).then((r) => r.text());
const json: SendResponse<R>[T] = JSON.parse(response);
const json: SendResponse<R, M>[T] = JSON.parse(response);

if (json.response && !option.skipDecrypt) {
const decrypted = this.decrypt(String(json.response), headers['X-timestamp']);
Expand Down Expand Up @@ -334,7 +349,7 @@ export class Fetcher {
metadata: { code: +code, message },
metaData: { code, message },
response: undefined
} as unknown as SendResponse<R>[T];
} as unknown as SendResponse<R, M>[T];
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Antrean } from './antrean.js';
import { Aplicares } from './aplicares.js';
import { Apotek } from './apotek/index.js';
import { CachedApi } from './base.js';
import { Fetcher } from './fetcher.js';
Expand All @@ -19,6 +20,10 @@ export default class JKN extends Fetcher {
return this.cache.get('antrean', Antrean);
}

get aplicares(): Aplicares {
return this.cache.get('aplicares', Aplicares);
}

get vclaim(): VClaim {
return VClaim.getInstance(this.cache);
}
Expand Down
2 changes: 1 addition & 1 deletion src/rekam-medis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class RekamMedis extends BaseApi<'rekamMedis'> {
*/
dataRekamMedis: Bundle<T>;
}) {
const config = await this.getConfig();
const config = await this.requiredConfig('ppkCode');
const dataMR = await preprocess(data.dataRekamMedis, config);
return this.send<{ keterangan: string }>({
path: `/eclaim/rekammedis/insert`,
Expand Down
Loading

0 comments on commit 69f1475

Please sign in to comment.