From be4257a95e82b3c23a8017a5cfb0e04cce67b381 Mon Sep 17 00:00:00 2001 From: Mustofa Date: Tue, 16 Jan 2024 12:41:24 +0700 Subject: [PATCH] fix: clarify EMR types (#78) * remove `@types/fhir` dependency * update readme * remove `@types/fhir` usages * tidy up utils * type: composition * finalize types * Revert "update readme" This reverts commit 8d37c62dd1cfdded14138dd777656844f230e353. * add changeset --- .changeset/nervous-doors-cry.md | 5 ++ package.json | 4 +- pnpm-lock.yaml | 12 ++-- src/rekam-medis/index.ts | 19 +++--- src/rekam-medis/types.ts | 102 ++++++++++++++++++++++++++------ src/rekam-medis/utils.ts | 14 ++--- words.txt | 1 + 7 files changed, 110 insertions(+), 47 deletions(-) create mode 100644 .changeset/nervous-doors-cry.md diff --git a/.changeset/nervous-doors-cry.md b/.changeset/nervous-doors-cry.md new file mode 100644 index 0000000..7a37af6 --- /dev/null +++ b/.changeset/nervous-doors-cry.md @@ -0,0 +1,5 @@ +--- +"@ssecd/jkn": patch +--- + +Fix Rekam Medis bundle types diff --git a/package.json b/package.json index 6c98206..447d12a 100644 --- a/package.json +++ b/package.json @@ -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", @@ -59,6 +58,9 @@ "dependencies": { "lz-string": "^1.5.0" }, + "peerDependencies": { + "@types/fhir": "^0.0.40" + }, "packageManager": "pnpm@8.6.1", "engines": { "pnpm": "^8.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 352db79..a769f9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@types/fhir': + specifier: ^0.0.40 + version: 0.0.40 lz-string: specifier: ^1.5.0 version: 1.5.0 @@ -13,9 +16,6 @@ devDependencies: '@changesets/cli': specifier: ^2.27.1 version: 2.27.1 - '@types/fhir': - specifier: ^0.0.38 - version: 0.0.38 '@types/node': specifier: ^20.10.5 version: 20.10.5 @@ -584,9 +584,9 @@ packages: resolution: {integrity: sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==} dev: true - /@types/fhir@0.0.38: - resolution: {integrity: sha512-0ia8Ng9OR6yI3J1+TkZxAzp02oarQLfM0mGBUjVf1+7X+hD9UW7TNz7/heRP5QQ9evyRz89Ok/IxSuubx2TzMg==} - dev: true + /@types/fhir@0.0.40: + resolution: {integrity: sha512-ae00uDa0GrgPl4sDsGpHEdUjxCeot0UEEhgO/4PljimMKrPMyEVMZpsiAjwCp+dARn7zybOflnLK+nfORLjxDw==} + dev: false /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} diff --git a/src/rekam-medis/index.ts b/src/rekam-medis/index.ts index 3510abe..2f740da 100644 --- a/src/rekam-medis/index.ts +++ b/src/rekam-medis/index.ts @@ -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(data: { + async insert(data: { /** nomor SEP */ nomorSEP: string; @@ -25,7 +25,7 @@ export class RekamMedis extends BaseApi<'rekamMedis'> { * Proses kompresi dan enkripsi akan dilakukan * secara otomatis pada method ini */ - dataRekamMedis: Bundle; + dataRekamMedis: MRBundle; }) { const config = await this.requiredConfig('ppkCode'); const dataMR = await preprocess(data.dataRekamMedis, config); @@ -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(data: Bundle, config: Config): Promise { +async function preprocess(data: MRBundle, { consId, consSecret, ppkCode }: Config): Promise { 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}`); } } diff --git a/src/rekam-medis/types.ts b/src/rekam-medis/types.ts index 64910ae..9227202 100644 --- a/src/rekam-medis/types.ts +++ b/src/rekam-medis/types.ts @@ -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 { + 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 & { + doseQuantity: Omit & { 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 { + clinicalStatus: string; + verificationStatus: string; +} -export interface DiagnosticReport extends fhir4.DiagnosticReport {} +interface Observation extends Omit { + 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 { + 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 extends fhir4.Bundle {} +export interface MRBundle + extends fhir4.Bundle< + | Composition + | Patient + | Encounter + | Practitioner + | Organization + | Condition + | Array + > {} diff --git a/src/rekam-medis/utils.ts b/src/rekam-medis/utils.ts index 8a2f4a1..de3589c 100644 --- a/src/rekam-medis/utils.ts +++ b/src/rekam-medis/utils.ts @@ -10,16 +10,10 @@ export async function gzip(value: string): Promise { }); } -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'); } diff --git a/words.txt b/words.txt index ecee4bf..fb5ef3d 100644 --- a/words.txt +++ b/words.txt @@ -13,6 +13,7 @@ BYTAGRSP BYVERRSP carakeluar checkstock +Codeable createdtime daftarfp daftarresep