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.
- package/package.json +1 -1
- package/src/index.js +143 -89
package/package.json
CHANGED
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 ||
|
|
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
|
|
31
|
-
|
|
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
|
|
38
|
-
const buf = Buffer.from(base64Str.replace(/\r|\n/g, ''), 'base64');
|
|
39
|
-
let xmlStr = null;
|
|
40
|
-
|
|
40
|
+
async _decodeBase64ToXml(base64Str) {
|
|
41
41
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
59
|
+
async _fetchAndParseXmlFromUrl(url) {
|
|
48
60
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
return { status:
|
|
165
|
-
|
|
166
|
-
|
|
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
|
}
|