soyba-lib 1.0.4 → 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 +138 -93
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soyba-lib",
3
- "version": "1.0.4",
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,92 +21,105 @@ 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
- async _decodeAndParseFiles(obj) {
58
- const results = [];
59
- const self = this;
82
+ async _processNestedXmlFiles(obj) {
83
+ if (!obj || typeof obj !== 'object') return obj;
60
84
 
61
- async function traverse(node) {
62
- 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
+ }
63
93
 
64
- if (node.FILEHOSO) {
65
- const files = Array.isArray(node.FILEHOSO) ? node.FILEHOSO : [node.FILEHOSO];
66
- const newFiles = [];
67
- for (const file of files) {
68
- if (file && file.NOIDUNGFILE && file.LOAIHOSO && self._isBase64(file.NOIDUNGFILE)) {
69
- try {
70
- const decoded = await self._decodeFileContent(file.NOIDUNGFILE);
71
- const minimal = {
72
- LOAIHOSO: file.LOAIHOSO,
73
- NOIDUNGFILE_JSON: decoded.json
74
- };
75
- results.push(minimal);
76
- newFiles.push(minimal);
77
- self.logger.debug('Decoded FILEHOSO and injected minimal file', { LOAIHOSO: file.LOAIHOSO });
78
- } catch (e) {
79
- self.logger.warn('Failed to decode FILEHOSO', { LOAIHOSO: file.LOAIHOSO, error: e.message });
80
- }
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
+ });
81
109
  } else {
82
- self.logger.debug('Skipping non-base64 or missing FILEHOSO', { maybeFile: file && file.LOAIHOSO });
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);
113
+ result[key] = processedFiles;
114
+ } else if (typeof value === 'object') {
115
+ result[key] = await this._processNestedXmlFiles(value);
116
+ } else {
117
+ result[key] = value;
88
118
  }
89
-
90
- for (const key of Object.keys(node)) {
91
- if (typeof node[key] === 'object') await traverse(node[key]);
92
- }
93
- }
94
-
95
- await traverse(obj);
96
- this.logger.debug('Total decoded FILEHOSO', { count: results.length });
97
- return results;
98
- }
99
-
100
- async _parseRawXml(xmlStr) {
101
- try {
102
- const json = await parseStringPromise(xmlStr, { explicitArray: false, mergeAttrs: true });
103
- return json;
104
- } catch (e) {
105
- this.logger.warn('Failed to parse raw XML', { error: e.message });
106
- return null;
107
119
  }
120
+ return result;
108
121
  }
109
122
 
110
- // Copy exact logic from soyba.service.js getLichSuKham
111
123
  async getLichSuKham(cccd, token) {
112
124
  const correlationId = `soyba_hist_${Date.now()}_${Math.random().toString(36).slice(2,9)}`;
113
125
 
@@ -123,18 +135,23 @@ class YBAClient {
123
135
 
124
136
  try {
125
137
  this.logger.debug('SOYBA getLichSuKham request', { correlationId, url, params: { cccd } });
126
- const resp = await axios.get(url, { headers, params: { cccd }, timeout: this.timeout });
127
- this.logger.debug('SOYBA getLichSuKham response', { correlationId, data: resp.data });
128
- return { status: resp.status, data: resp.data };
129
- } catch (err) {
130
- this.logger.error('SOYBA getLichSuKham failed', { correlationId, error: err.message, stack: err.stack, url, params: { cccd } });
131
- const status = err.response?.status || 500;
132
- const data = err.response?.data || { error: 'SOYBA request failed' };
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' };
133
151
  return { status, data };
134
152
  }
135
153
  }
136
154
 
137
- // Copy exact logic from soyba.service.js getChiTietLichSu
138
155
  async getChiTietLichSu(cccd, ma_lk, token) {
139
156
  const correlationId = `soyba_detail_${Date.now()}_${Math.random().toString(36).slice(2,9)}`;
140
157
 
@@ -150,29 +167,57 @@ class YBAClient {
150
167
 
151
168
  try {
152
169
  this.logger.debug('SOYBA getChiTietLichSu request', { correlationId, url, params: { cccd, ma_lk } });
153
- const resp = await axios.get(url, { headers, params: { cccd, ma_lk }, timeout: this.timeout });
154
- this.logger.debug('YBA raw response', { correlationId, data: resp.data });
155
-
156
- // Parse XML string if needed
157
- let rootObj = resp.data;
158
- if (typeof resp.data === 'string') {
159
- const parsed = await this._parseRawXml(resp.data);
160
- if (parsed) rootObj = parsed;
161
- }
162
-
163
- // Decode FILEHOSO files
164
- const decodedFiles = await this._decodeAndParseFiles(rootObj);
165
-
166
- if (decodedFiles.length > 0) {
167
- // Return exactly like service gốc
168
- return { status: resp.status, data: { original: rootObj } };
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);
169
206
  }
170
-
171
- // No decoded files - return parsed object or fallback
172
- return { status: resp.status, data: rootObj || [] };
173
- } catch (err) {
174
- this.logger.error('SOYBA getChiTietLichSu failed', { correlationId, error: err.message, stack: err.stack, url, params: { cccd, ma_lk } });
175
- 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;
176
221
  return { status, data: [] };
177
222
  }
178
223
  }