soyba-lib 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +10 -0
  2. package/src/index.js +199 -0
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "soyba-lib",
3
+ "version": "1.0.0",
4
+ "main": "src/index.js",
5
+ "type": "module",
6
+ "description": "Thư viện YBA (Sổ Y Bạ) - Truy vấn lịch sử khám bệnh",
7
+ "keywords": ["yba", "soyba", "medical", "history"],
8
+ "author": "hospital-backend",
9
+ "license": "MIT"
10
+ }
package/src/index.js ADDED
@@ -0,0 +1,199 @@
1
+ import axios from 'axios';
2
+ import { parseStringPromise } from 'xml2js';
3
+ import zlib from 'zlib';
4
+
5
+ class YBAClient {
6
+ constructor(options = {}) {
7
+ this.baseUrl = options.baseUrl || process.env.API_SOYBA;
8
+ this.apiKey = options.apiKey || process.env.API_KEY;
9
+ this.timeout = options.timeout || 15000;
10
+
11
+ if (!this.baseUrl) {
12
+ throw new Error('YBA baseUrl is required');
13
+ }
14
+ }
15
+
16
+ _buildAuthHeader(token) {
17
+ const apiKey = this.apiKey || '';
18
+ if (token) return `${token}${apiKey}`;
19
+ return apiKey ? `token ${apiKey}` : undefined;
20
+ }
21
+
22
+ _isBase64(str) {
23
+ if (typeof str !== 'string') return false;
24
+ const cleaned = str.replace(/\s+/g, '');
25
+ if (!/^[A-Za-z0-9+/=_-]+$/.test(cleaned)) return false;
26
+ try {
27
+ const buf = Buffer.from(cleaned, 'base64');
28
+ return buf && buf.length > 0;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ async _decodeFileContent(base64Str) {
35
+ const buf = Buffer.from(base64Str.replace(/\r|\n/g, ''), 'base64');
36
+ let xmlStr = null;
37
+
38
+ try {
39
+ xmlStr = zlib.gunzipSync(buf).toString('utf8');
40
+ } catch {
41
+ xmlStr = buf.toString('utf8');
42
+ }
43
+
44
+ let json = null;
45
+ try {
46
+ json = await parseStringPromise(xmlStr, { explicitArray: false, mergeAttrs: true });
47
+ } catch {
48
+ // XML không hợp lệ
49
+ }
50
+
51
+ return { xml: xmlStr, json };
52
+ }
53
+
54
+ async _decodeAndParseFiles(obj) {
55
+ const results = [];
56
+
57
+ const traverse = async (node) => {
58
+ if (!node || typeof node !== 'object') return;
59
+
60
+ if (node.FILEHOSO) {
61
+ const files = Array.isArray(node.FILEHOSO) ? node.FILEHOSO : [node.FILEHOSO];
62
+ const newFiles = [];
63
+
64
+ for (const file of files) {
65
+ if (file && file.NOIDUNGFILE && file.LOAIHOSO && this._isBase64(file.NOIDUNGFILE)) {
66
+ try {
67
+ const decoded = await this._decodeFileContent(file.NOIDUNGFILE);
68
+ const minimal = {
69
+ LOAIHOSO: file.LOAIHOSO,
70
+ NOIDUNGFILE_JSON: decoded.json
71
+ };
72
+ results.push(minimal);
73
+ newFiles.push(minimal);
74
+ } catch (e) {
75
+ // Skip decode error
76
+ }
77
+ }
78
+ }
79
+
80
+ node.FILEHOSO = Array.isArray(node.FILEHOSO) ? newFiles : (newFiles[0] || undefined);
81
+ }
82
+
83
+ for (const key of Object.keys(node)) {
84
+ if (typeof node[key] === 'object') await traverse(node[key]);
85
+ }
86
+ };
87
+
88
+ await traverse(obj);
89
+ return results;
90
+ }
91
+
92
+ async _parseRawXml(xmlStr) {
93
+ try {
94
+ const json = await parseStringPromise(xmlStr, { explicitArray: false, mergeAttrs: true });
95
+ return json;
96
+ } catch (e) {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ async getLichSuKham(cccd, token) {
102
+ if (!cccd) {
103
+ return { success: false, message: 'Tham số cccd là bắt buộc', data: null };
104
+ }
105
+
106
+ const url = `${this.baseUrl.replace(/\/$/, '')}/api/yba`;
107
+ const headers = { 'Content-Type': 'application/json' };
108
+ const auth = this._buildAuthHeader(token);
109
+ if (auth) headers.Authorization = auth;
110
+
111
+ try {
112
+ const resp = await axios.get(url, {
113
+ headers,
114
+ params: { cccd },
115
+ timeout: this.timeout
116
+ });
117
+
118
+ return {
119
+ success: true,
120
+ message: 'Lấy lịch sử khám thành công',
121
+ data: resp.data
122
+ };
123
+ } catch (err) {
124
+ const status = err.response?.status || 500;
125
+ const errorData = err.response?.data;
126
+
127
+ return {
128
+ success: false,
129
+ message: 'Lấy lịch sử khám thất bại',
130
+ data: errorData,
131
+ error: err.message,
132
+ status
133
+ };
134
+ }
135
+ }
136
+
137
+ async getChiTietLichSu(cccd, ma_lk, token) {
138
+ if (!cccd || !ma_lk) {
139
+ return {
140
+ success: false,
141
+ message: 'Tham số cccd và ma_lk là bắt buộc',
142
+ data: null
143
+ };
144
+ }
145
+
146
+ const url = `${this.baseUrl.replace(/\/$/, '')}/api/yba/lk`;
147
+ const headers = { 'Content-Type': 'application/json' };
148
+ const auth = this._buildAuthHeader(token);
149
+ if (auth) headers.Authorization = auth;
150
+
151
+ try {
152
+ const resp = await axios.get(url, {
153
+ headers,
154
+ params: { cccd, ma_lk },
155
+ timeout: this.timeout
156
+ });
157
+
158
+ // Parse XML string nếu cần
159
+ let rootObj = resp.data;
160
+ if (typeof resp.data === 'string') {
161
+ const parsed = await this._parseRawXml(resp.data);
162
+ if (parsed) rootObj = parsed;
163
+ }
164
+
165
+ // Decode FILEHOSO nếu có
166
+ const decodedFiles = await this._decodeAndParseFiles(rootObj);
167
+
168
+ if (decodedFiles.length > 0) {
169
+ // Xóa GIAMDINHHS nếu có
170
+ if (rootObj && rootObj.GIAMDINHHS) delete rootObj.GIAMDINHHS;
171
+ return {
172
+ success: true,
173
+ message: 'Lấy chi tiết lịch sử khám thành công',
174
+ data: rootObj
175
+ };
176
+ }
177
+
178
+ return {
179
+ success: true,
180
+ message: 'Lấy chi tiết lịch sử khám thành công',
181
+ data: rootObj || []
182
+ };
183
+
184
+ } catch (err) {
185
+ const status = err.response?.status || 500;
186
+ const errorData = err.response?.data;
187
+
188
+ return {
189
+ success: false,
190
+ message: 'Lấy chi tiết lịch sử khám thất bại',
191
+ data: errorData,
192
+ error: err.message,
193
+ status
194
+ };
195
+ }
196
+ }
197
+ }
198
+
199
+ export default YBAClient;