soyba-lib 1.0.3 → 1.0.5

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 +1 -1
  2. package/src/index.js +143 -89
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soyba-lib",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "description": "Thư viện YBA (Sổ Y Bạ) - Truy vấn lịch sử khám bệnh",
package/src/index.js CHANGED
@@ -1,13 +1,12 @@
1
1
  // lib/soyba/src/index.js
2
2
  import axios from 'axios';
3
3
  import { parseStringPromise } from 'xml2js';
4
- import zlib from 'zlib';
5
4
 
6
5
  class YBAClient {
7
6
  constructor(options = {}) {
8
7
  this.baseUrl = options.baseUrl || process.env.API_SOYBA;
9
8
  this.apiKey = options.apiKey || process.env.API_KEY;
10
- this.timeout = options.timeout || 15000;
9
+ this.timeout = options.timeout || 30000;
11
10
  this.logger = options.logger || console;
12
11
 
13
12
  if (!this.baseUrl) {
@@ -22,94 +21,110 @@ class YBAClient {
22
21
  }
23
22
 
24
23
  _isBase64(str) {
25
- if (typeof str !== 'string') return false;
24
+ if (typeof str !== 'string' || str.length === 0) return false;
26
25
 
26
+ // Remove whitespace and check format
27
27
  const cleaned = str.replace(/\s+/g, '');
28
28
  if (!/^[A-Za-z0-9+/=_-]+$/.test(cleaned)) return false;
29
+
30
+ // Check if it's a valid base64
29
31
  try {
30
- const buf = Buffer.from(cleaned, 'base64');
31
- return buf && buf.length > 0;
32
+ const decoded = Buffer.from(cleaned, 'base64').toString('utf8');
33
+ // Check if it looks like XML
34
+ return decoded.includes('<') && decoded.includes('>');
32
35
  } catch {
33
36
  return false;
34
37
  }
35
38
  }
36
39
 
37
- async _decodeFileContent(base64Str) {
38
- const buf = Buffer.from(base64Str.replace(/\r|\n/g, ''), 'base64');
39
- let xmlStr = null;
40
-
40
+ async _decodeBase64ToXml(base64Str) {
41
41
  try {
42
- xmlStr = zlib.gunzipSync(buf).toString('utf8');
43
- } catch {
44
- xmlStr = buf.toString('utf8');
42
+ const cleaned = base64Str.replace(/\s+/g, '');
43
+ const xmlStr = Buffer.from(cleaned, 'base64').toString('utf8');
44
+
45
+ // Parse XML to JSON
46
+ const json = await parseStringPromise(xmlStr, {
47
+ explicitArray: false,
48
+ mergeAttrs: true,
49
+ trim: true
50
+ });
51
+
52
+ return { xml: xmlStr, json };
53
+ } catch (error) {
54
+ this.logger.warn('Failed to decode base64 XML', { error: error.message });
55
+ return { xml: null, json: null };
45
56
  }
57
+ }
46
58
 
47
- let json = null;
59
+ async _fetchAndParseXmlFromUrl(url) {
48
60
  try {
49
- json = await parseStringPromise(xmlStr, { explicitArray: false, mergeAttrs: true });
50
- } catch {
51
- // XML parsing failed
61
+ this.logger.debug('Fetching XML from URL', { url });
62
+
63
+ const response = await axios.get(url, {
64
+ timeout: this.timeout,
65
+ responseType: 'text'
66
+ });
67
+
68
+ // Parse XML to JSON
69
+ const json = await parseStringPromise(response.data, {
70
+ explicitArray: false,
71
+ mergeAttrs: true,
72
+ trim: true
73
+ });
74
+
75
+ return { xml: response.data, json };
76
+ } catch (error) {
77
+ this.logger.warn('Failed to fetch/parse XML from URL', { url, error: error.message });
78
+ return { xml: null, json: null };
52
79
  }
53
-
54
- return { xml: xmlStr, json };
55
80
  }
56
81
 
57
- // lib/soyba/src/index.js
58
- async _decodeAndParseFiles(obj) {
59
- const results = [];
60
- const self = this; // ✅ Lưu reference đến this
82
+ async _processNestedXmlFiles(obj) {
83
+ if (!obj || typeof obj !== 'object') return obj;
61
84
 
62
- async function traverse(node) {
63
- if (!node || typeof node !== 'object') return;
85
+ // Process arrays
86
+ if (Array.isArray(obj)) {
87
+ const processed = [];
88
+ for (const item of obj) {
89
+ processed.push(await this._processNestedXmlFiles(item));
90
+ }
91
+ return processed;
92
+ }
64
93
 
65
- if (node.FILEHOSO) {
66
- const files = Array.isArray(node.FILEHOSO) ? node.FILEHOSO : [node.FILEHOSO];
67
- const newFiles = [];
68
- for (const file of files) {
69
- if (file && file.NOIDUNGFILE && file.LOAIHOSO && self._isBase64(file.NOIDUNGFILE)) {
70
- // ^^^^^ Dùng self thay vì this
71
- try {
72
- const decoded = await self._decodeFileContent(file.NOIDUNGFILE);
73
- // ^^^^^ Dùng self
74
- const minimal = {
75
- LOAIHOSO: file.LOAIHOSO,
76
- NOIDUNGFILE_JSON: decoded.json
77
- };
78
- results.push(minimal);
79
- newFiles.push(minimal);
80
- } catch (e) {
81
- // Skip decode error
82
- }
94
+ // Process objects
95
+ const result = {};
96
+ for (const [key, value] of Object.entries(obj)) {
97
+ if (key === 'FILEHOSO' && Array.isArray(value)) {
98
+ // Process FILEHOSO array - decode NOIDUNGFILE in each item
99
+ const processedFiles = [];
100
+ for (const file of value) {
101
+ if (file && file.NOIDUNGFILE && this._isBase64(file.NOIDUNGFILE)) {
102
+ this.logger.debug('Decoding XML file', { LOAIHOSO: file.LOAIHOSO });
103
+ const decoded = await this._decodeBase64ToXml(file.NOIDUNGFILE);
104
+ processedFiles.push({
105
+ ...file,
106
+ NOIDUNGFILE: decoded.json || decoded.xml || file.NOIDUNGFILE,
107
+ NOIDUNGFILE_RAW: decoded.xml
108
+ });
109
+ } else {
110
+ processedFiles.push(file);
83
111
  }
84
112
  }
85
-
86
- // Replace FILEHOSO with decoded files
87
- node.FILEHOSO = Array.isArray(node.FILEHOSO) ? newFiles : (newFiles[0] || undefined);
88
- }
89
-
90
- for (const key of Object.keys(node)) {
91
- if (typeof node[key] === 'object') await traverse(node[key]); // ✅ Bỏ .call(this, ...)
113
+ result[key] = processedFiles;
114
+ } else if (typeof value === 'object') {
115
+ result[key] = await this._processNestedXmlFiles(value);
116
+ } else {
117
+ result[key] = value;
92
118
  }
93
119
  }
94
-
95
- await traverse(obj); // ✅ Bỏ .call(this, ...)
96
- return results;
120
+ return result;
97
121
  }
98
122
 
99
- async _parseRawXml(xmlStr) {
100
- try {
101
- const json = await parseStringPromise(xmlStr, { explicitArray: false, mergeAttrs: true });
102
- return json;
103
- } catch (e) {
104
- return null;
105
- }
106
- }
107
-
108
- // Copy exact logic from soyba.service.js getLichSuKham
109
123
  async getLichSuKham(cccd, token) {
110
124
  const correlationId = `soyba_hist_${Date.now()}_${Math.random().toString(36).slice(2,9)}`;
111
125
 
112
126
  if (!this.baseUrl) {
127
+ this.logger.error('SOYBA base url not configured', { correlationId });
113
128
  throw new Error('Missing API_SOYBA in environment');
114
129
  }
115
130
 
@@ -119,20 +134,29 @@ class YBAClient {
119
134
  if (auth) headers.Authorization = auth;
120
135
 
121
136
  try {
122
- const resp = await axios.get(url, { headers, params: { cccd }, timeout: this.timeout });
123
- return { status: resp.status, data: resp.data };
124
- } catch (err) {
125
- const status = err.response?.status || 500;
126
- const data = err.response?.data || { error: 'SOYBA request failed' };
137
+ this.logger.debug('SOYBA getLichSuKham request', { correlationId, url, params: { cccd } });
138
+ const response = await axios.get(url, { headers, params: { cccd }, timeout: this.timeout });
139
+ this.logger.debug('SOYBA getLichSuKham response', { correlationId, status: response.status });
140
+
141
+ return { status: response.status, data: response.data };
142
+ } catch (error) {
143
+ this.logger.error('SOYBA getLichSuKham failed', {
144
+ correlationId,
145
+ error: error.message,
146
+ status: error.response?.status
147
+ });
148
+
149
+ const status = error.response?.status || 500;
150
+ const data = error.response?.data || { error: 'SOYBA request failed' };
127
151
  return { status, data };
128
152
  }
129
153
  }
130
154
 
131
- // Copy exact logic from soyba.service.js getChiTietLichSu
132
155
  async getChiTietLichSu(cccd, ma_lk, token) {
133
156
  const correlationId = `soyba_detail_${Date.now()}_${Math.random().toString(36).slice(2,9)}`;
134
157
 
135
158
  if (!this.baseUrl) {
159
+ this.logger.error('SOYBA base url not configured', { correlationId });
136
160
  throw new Error('Missing API_SOYBA in environment');
137
161
  }
138
162
 
@@ -142,28 +166,58 @@ class YBAClient {
142
166
  if (auth) headers.Authorization = auth;
143
167
 
144
168
  try {
145
- const resp = await axios.get(url, { headers, params: { cccd, ma_lk }, timeout: this.timeout });
146
-
147
- // Parse XML string if needed
148
- let rootObj = resp.data;
149
- if (typeof resp.data === 'string') {
150
- const parsed = await this._parseRawXml(resp.data);
151
- if (parsed) rootObj = parsed;
152
- }
153
-
154
- // Decode FILEHOSO files
155
- const decodedFiles = await this._decodeAndParseFiles(rootObj);
156
-
157
- if (decodedFiles.length > 0) {
158
- // Remove GIAMDINHHS if present
159
- if (rootObj && rootObj.GIAMDINHHS) delete rootObj.GIAMDINHHS;
160
- return { status: resp.status, data: rootObj };
169
+ this.logger.debug('SOYBA getChiTietLichSu request', { correlationId, url, params: { cccd, ma_lk } });
170
+ const response = await axios.get(url, { headers, params: { cccd, ma_lk }, timeout: this.timeout });
171
+
172
+ this.logger.debug('YBA raw response received', { correlationId, dataLength: JSON.stringify(response.data).length });
173
+
174
+ let processedData = response.data;
175
+
176
+ // Handle different response formats
177
+ if (Array.isArray(processedData)) {
178
+ // Process each item in array
179
+ const processedArray = [];
180
+ for (const item of processedData) {
181
+ if (item.Url && item.ContentType === 'text/xml') {
182
+ // New format: fetch XML from URL
183
+ this.logger.debug('Fetching XML from URL', { Url: item.Url });
184
+ const xmlResult = await this._fetchAndParseXmlFromUrl(item.Url);
185
+ if (xmlResult.json) {
186
+ // Process nested XML files in the fetched content
187
+ const processedXml = await this._processNestedXmlFiles(xmlResult.json);
188
+ processedArray.push({
189
+ ...item,
190
+ NOIDUNGFILE: processedXml,
191
+ NOIDUNGFILE_RAW: xmlResult.xml
192
+ });
193
+ } else {
194
+ processedArray.push(item);
195
+ }
196
+ } else {
197
+ // Process existing NOIDUNGFILE if present
198
+ const processedItem = await this._processNestedXmlFiles(item);
199
+ processedArray.push(processedItem);
200
+ }
201
+ }
202
+ processedData = processedArray;
203
+ } else if (processedData && typeof processedData === 'object') {
204
+ // Single object - process nested files
205
+ processedData = await this._processNestedXmlFiles(processedData);
161
206
  }
162
-
163
- // No decoded files - return parsed object or fallback
164
- return { status: resp.status, data: rootObj || [] };
165
- } catch (err) {
166
- const status = err.response?.status || 500;
207
+
208
+ this.logger.debug('YBA processing completed', { correlationId });
209
+ return { status: response.status, data: processedData };
210
+
211
+ } catch (error) {
212
+ this.logger.error('SOYBA getChiTietLichSu failed', {
213
+ correlationId,
214
+ error: error.message,
215
+ status: error.response?.status,
216
+ url,
217
+ params: { cccd, ma_lk }
218
+ });
219
+
220
+ const status = error.response?.status || 500;
167
221
  return { status, data: [] };
168
222
  }
169
223
  }