Skip to content

Commit

Permalink
fix: clarify EMR types (#78)
Browse files Browse the repository at this point in the history
* remove `@types/fhir` dependency

* update readme

* remove `@types/fhir` usages

* tidy up utils

* type: composition

* finalize types

* Revert "update readme"

This reverts commit 8d37c62.

* add changeset
  • Loading branch information
mustofa-id authored Jan 16, 2024
1 parent 57dc9ad commit be4257a
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-doors-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ssecd/jkn": patch
---

Fix Rekam Medis bundle types
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@types/fhir": "^0.0.38",
"@types/node": "^20.10.5",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
Expand All @@ -59,6 +58,9 @@
"dependencies": {
"lz-string": "^1.5.0"
},
"peerDependencies": {
"@types/fhir": "^0.0.40"
},
"packageManager": "[email protected]",
"engines": {
"pnpm": "^8.0.0"
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 8 additions & 11 deletions src/rekam-medis/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { BaseApi } from '../base.js';
import { Config } from '../fetcher.js';
import { Bundle, JKNFhirResource } from './types.js';
import { MRBundle } from './types.js';
import { encrypt, gzip } from './utils.js';

export class RekamMedis extends BaseApi<'rekamMedis'> {
protected type = 'rekamMedis' as const;

async insert<T = JKNFhirResource>(data: {
async insert(data: {
/** nomor SEP */
nomorSEP: string;

Expand All @@ -25,7 +25,7 @@ export class RekamMedis extends BaseApi<'rekamMedis'> {
* Proses kompresi dan enkripsi akan dilakukan
* secara otomatis pada method ini
*/
dataRekamMedis: Bundle<T>;
dataRekamMedis: MRBundle;
}) {
const config = await this.requiredConfig('ppkCode');
const dataMR = await preprocess(data.dataRekamMedis, config);
Expand Down Expand Up @@ -54,17 +54,14 @@ export class RekamMedis extends BaseApi<'rekamMedis'> {
* dan KODE PPK. Ini berdasarkan spesifikasi yang telah ditentukan
* pada halaman TrustMark BPJS Kesehatan.
*/
async function preprocess<T>(data: Bundle<T>, config: Config): Promise<string> {
async function preprocess(data: MRBundle, { consId, consSecret, ppkCode }: Config): Promise<string> {
try {
const value = JSON.stringify(data);
const compressed = await gzip(value);
return encrypt(compressed.toString('base64'), [
config.consId,
config.consSecret,
config.ppkCode
]);
const key = consId + consSecret + ppkCode;
return encrypt(compressed.toString('base64'), key);
} catch (err) {
// TODO: define custom error
throw new Error(`failed to compress or encrypt data. ${err}`);
const message = err instanceof Error ? err.message : JSON.stringify(err);
throw new Error(`failed to compress or encrypt data. ${message}`);
}
}
102 changes: 83 additions & 19 deletions src/rekam-medis/types.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,99 @@
export interface Composition extends fhir4.Composition {}
/*
* Unfortunately, we are unable to fully utilize `@types/fhir` here because BPJS
* TrustMark does not seem to be adhering to the standard FHIR implementation.
*/

export interface Composition extends Omit<fhir4.Composition, 'section'> {
section: {
[key: string]: fhir4.CompositionSection;
};
}

export interface Patient extends fhir4.Patient {}

export interface Encounter extends fhir4.Encounter {
subject?: fhir4.Encounter['subject'] & { noSep: string };
subject: fhir4.Reference & { noSep: string };
incomingReferral: { identifier: fhir4.Identifier[] }[];
reason: fhir4.CodeableConcept[];
diagnosis?:
| (fhir4.EncounterDiagnosis & {
condition: fhir4.Reference & { role: fhir4.CodeableConcept; rank: number };
})[]
| undefined;
hospitalization:
| (fhir4.EncounterHospitalization & {
dischargeDisposition?: fhir4.CodeableConcept[] | undefined;
})
| undefined;
}

export interface MedicationRequest extends fhir4.MedicationRequest {}
export interface MedicationRequest
extends Omit<
fhir4.MedicationRequest,
'identifier' | 'intent' | 'dosageInstruction' | 'requester'
> {
identifier?: fhir4.Identifier;
intent: fhir4.MedicationRequest['intent'] | 'final';
dosageInstruction?: (Omit<fhir4.Dosage, 'timing'> & {
doseQuantity: Omit<fhir4.Quantity, 'value'> & { value: string };
timing: {
repeat: { frequency: string; periodUnit: string; period: number };
};
})[];
requester: {
agent: fhir4.Reference;
onBehalfOf: fhir4.Reference;
};
}

export interface Practitioner extends fhir4.Practitioner {}

export interface Organization extends fhir4.Organization {}

export interface Condition extends fhir4.Condition {}
export interface Condition extends Omit<fhir4.Condition, 'clinicalStatus' | 'verificationStatus'> {
clinicalStatus: string;
verificationStatus: string;
}

export interface DiagnosticReport extends fhir4.DiagnosticReport {}
interface Observation extends Omit<fhir4.Observation, 'performer' | 'code'> {
performer?: fhir4.Reference;
code?: { coding: fhir4.Coding; text: string };
image: {
comment: string;
link: {
reference: string;
display: string;
};
}[];
conclusion?: string;
}

export interface Procedure extends fhir4.Procedure {}
export interface DiagnosticReport
extends Omit<fhir4.DiagnosticReport, 'category' | 'result' | 'code'> {
subject: fhir4.Reference & { noSep: string };
category?: {
coding: fhir4.Coding;
};
result?: Observation[];
code?: fhir4.CodeableConcept;
}

export interface Device extends fhir4.Device {}
export interface Procedure extends fhir4.Procedure {
context: fhir4.Reference;
performer: (fhir4.ProcedurePerformer & { role: fhir4.CodeableConcept })[];
}

export type JKNFhirResource =
| Composition
| Patient
| Encounter
| MedicationRequest
| Practitioner
| Organization
| Condition
| DiagnosticReport
| Procedure
| Device;
export interface Device extends fhir4.Device {
model: string;
}

export interface Bundle<T = JKNFhirResource> extends fhir4.Bundle<T> {}
export interface MRBundle
extends fhir4.Bundle<
| Composition
| Patient
| Encounter
| Practitioner
| Organization
| Condition
| Array<MedicationRequest | DiagnosticReport | Procedure | Device>
> {}
14 changes: 4 additions & 10 deletions src/rekam-medis/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,10 @@ export async function gzip(value: string): Promise<Buffer> {
});
}

export function encrypt(value: string, keyCombinations: string[]): string {
if (keyCombinations.some((k) => !k)) {
throw new Error(`consId, consSecret, or ppkCode are not set`);
}

const keyPlain = keyCombinations.join('');
const key = createHash('sha256').update(keyPlain, 'utf8').digest();
export function encrypt(value: string, plainKey: string): string {
const key = createHash('sha256').update(plainKey, 'utf8').digest();
const iv = Uint8Array.from(key.subarray(0, 16));
const cipher = createCipheriv('aes-256-cbc', key, iv);
let enc = cipher.update(value, 'utf8', 'base64');
enc += cipher.final('base64');
return enc;
const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
return encrypted.toString('base64');
}
1 change: 1 addition & 0 deletions words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ BYTAGRSP
BYVERRSP
carakeluar
checkstock
Codeable
createdtime
daftarfp
daftarresep
Expand Down

0 comments on commit be4257a

Please sign in to comment.