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.
- package/package.json +1 -1
- package/src/index.js +138 -93
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,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
|
|
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
|
-
async
|
|
58
|
-
|
|
59
|
-
const self = this;
|
|
82
|
+
async _processNestedXmlFiles(obj) {
|
|
83
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
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
|
-
|
|
110
|
+
processedFiles.push(file);
|
|
83
111
|
}
|
|
84
112
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
127
|
-
this.logger.debug('SOYBA getLichSuKham response', { correlationId,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
let
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
return { status:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
}
|