├── .gitignore ├── .env ├── package.json ├── README.md └── einvoice-sdk.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | X509Certificate_VALUE= 2 | X509SubjectName_VALUE= 3 | X509IssuerName_VALUE= 4 | X509SerialNumber_VALUE= 5 | 6 | 7 | CLIENT_ID_VALUE= 8 | CLIENT_SECRET_1_VALUE= 9 | 10 | PREPROD_BASE_URL= 11 | 12 | PRIVATE_KEY_FILE_PATH=example.key 13 | PRIVATE_CERT_FILE_PATH=exampleCert.crt 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "einvoice-sdk-nodejs", 3 | "version": "1.1.3", 4 | "main": "einvoice-sdk.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Syukran Soleh", 9 | "license": "ISC", 10 | "description": "A Node.js SDK for e-invoicing.", 11 | "dependencies": { 12 | "axios": "^1.7.2", 13 | "crypto": "^1.0.1", 14 | "crypto-js": "^4.2.0", 15 | "dotenv": "^16.4.5", 16 | "fs": "^0.0.1-security", 17 | "jsonminify": "^0.4.2", 18 | "node-forge": "^1.3.1", 19 | "path": "^0.12.7" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/syukranDev/e-invoice-sdk-nodejs" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/syukranDev/e-invoice-sdk-nodejs/issues" 27 | }, 28 | "homepage": "https://github.com/syukranDev/e-invoice-sdk-nodejs", 29 | "keywords": [ 30 | "e-invoice", 31 | "sdk", 32 | "nodejs", 33 | "open-source", 34 | "lhdn", 35 | "irb", 36 | "malaysia", 37 | "invois", 38 | "e-invois" 39 | ], 40 | "contributors": [ 41 | { 42 | "name": "Syukran Soleh", 43 | "email": "m.syukransoleh@gmail.com" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # einvoice-sdk-nodejs 2 | 3 | A Node.js SDK for interacting with e-invoice LHDN @ IRB MALAYSIA APIs using JSON format data, including obtaining tokens, submitting documents, and managing e-invoice data. 4 | 5 | ## Features 6 | 7 | Provides a set of functions to cater to the following needs. You may still need to plan the flow based on your business requirements: 8 | - Obtain tokens as a taxpayer or intermediary 9 | - Validate TIN using different ID type 10 | - Submit documents 11 | - Get document details 12 | - Cancel valid documents by supplier 13 | - Utility functions for JSON to Base64 conversion, SHA256 hash calculation, and generating certificate hashed parameters and hashed documents 14 | - Automatic API recall in case of hitting the API rate limit 15 | 16 | ## Usage 17 | Create a .env file in the root directory and add your configuration variables: 18 | ```bash 19 | CLIENT_ID_VALUE=your-client-id 20 | CLIENT_SECRET_1_VALUE=your-client-secret 21 | PREPROD_BASE_URL=your-preprod-base-url 22 | X509Certificate_VALUE=your-x509-certificate 23 | X509SubjectName_VALUE=your-x509-subject-name 24 | X509IssuerName_VALUE=your-x509-issuer-name 25 | X509SerialNumber_VALUE=your-x509-serial-number 26 | PRIVATE_KEY_FILE_PATH=example.key 27 | PRIVATE_CERT_FILE_PATH=exampleCert.crt 28 | ``` 29 | 30 | ```bash 31 | const einvois = require('./einvoice-sdk.js'); 32 | 33 | # Note: You may refer getCertificatesHashedParams() on how to generate hashed signed documents. 34 | # let hashed_payload = { 35 | # "documents": [ 36 | # { 37 | # "format": "JSON", 38 | # "documentHash": , 39 | # "codeNumber": , 40 | # "document": 41 | # } 42 | 43 | # ] 44 | # } 45 | 46 | try { 47 | const token = await einvois.getTokenAsTaxPayer(); 48 | const documentSubmissionResponse = await einvois.submitDocument(hashed_payload.documents, token.access_token); 49 | console.log(documentSubmissionResponse); 50 | } catch (error) { 51 | console.error(error); 52 | } 53 | 54 | ``` 55 | 56 | ## Contributing / License 57 | Author: Syukran Soleh
58 | This project is open-source and licensed under the ISC License. Contributions are welcome—please follow the guidelines for contributions and feel free to submit issues or pull requests. 59 | 60 | 61 | -------------------------------------------------------------------------------- /einvoice-sdk.js: -------------------------------------------------------------------------------- 1 | //DEV Note: Committing here for dev testing, please dont remove. 2 | const path = require('path') 3 | const axios = require('axios'); 4 | const CryptoJS = require('crypto-js'); 5 | const env = process.env.NODE_ENV || 'dev'; 6 | const fs = require('fs'); 7 | const forge = require('node-forge'); 8 | const jsonminify = require('jsonminify'); 9 | const crypto = require('crypto'); 10 | require('dotenv').config(); 11 | 12 | let httpOptions = { 13 | client_id: process.env.CLIENT_ID_VALUE, 14 | client_secret: process.env.CLIENT_SECRET_1_VALUE, 15 | grant_type: 'client_credentials', 16 | scope: 'InvoicingAPI' 17 | } 18 | 19 | async function getTokenAsTaxPayer() { 20 | try { 21 | 22 | const response = await axios.post(`${process.env.PREPROD_BASE_URL}/connect/token`, httpOptions, { 23 | headers: { 24 | 'Content-Type': 'application/x-www-form-urlencoded' 25 | } 26 | }); 27 | 28 | if(response.status == 200) return response.data; 29 | } catch (err) { 30 | if (err.response.status == 429) { 31 | console.log('A- Current iteration hitting Rate Limit 429 of LHDN Taxpayer Token API, retrying...') 32 | const rateLimitReset = err.response.headers["x-rate-limit-reset"]; 33 | 34 | if (rateLimitReset) { 35 | const resetTime = new Date(rateLimitReset).getTime(); 36 | const currentTime = Date.now(); 37 | const waitTime = resetTime - currentTime; 38 | 39 | if (waitTime > 0) { 40 | console.log('======================================================================================='); 41 | console.log(' LHDN Taxpayer Token API hitting rate limit HTTP 429 '); 42 | console.log(` Refetching................. (Waiting time: ${waitTime} ms) `); 43 | console.log('======================================================================================='); 44 | await new Promise(resolve => setTimeout(resolve, waitTime)); 45 | return await getTokenAsTaxPayer(); 46 | } 47 | } 48 | } else { 49 | throw new Error(`Failed to get token: ${err.message}`); 50 | } 51 | } 52 | } 53 | 54 | async function getTokenAsIntermediary() { 55 | try { 56 | const response = await axios.post(`${process.env.PREPROD_BASE_URL}/connect/token`, httpOptions, { 57 | headers: { 58 | 'onbehalfof':config.configDetails.tin, 59 | 'Content-Type': 'application/x-www-form-urlencoded' 60 | } 61 | }); 62 | 63 | if(response.status == 200) return response.data; 64 | } catch (err) { 65 | if (err.response.status == 429) { 66 | console.log('A- Current iteration hitting Rate Limit 429 of LHDN Intermediary Token API, retrying...') 67 | const rateLimitReset = err.response.headers["x-rate-limit-reset"]; 68 | 69 | if (rateLimitReset) { 70 | const resetTime = new Date(rateLimitReset).getTime(); 71 | const currentTime = Date.now(); 72 | const waitTime = resetTime - currentTime; 73 | 74 | if (waitTime > 0) { 75 | console.log('======================================================================================='); 76 | console.log(' LHDN Intermediary Token API hitting rate limit HTTP 429 '); 77 | console.log(` Refetching................. (Waiting time: ${waitTime} ms) `); 78 | console.log('======================================================================================='); 79 | await new Promise(resolve => setTimeout(resolve, waitTime)); 80 | return await getTokenAsIntermediary(); 81 | } 82 | } 83 | } else { 84 | throw new Error(`Failed to get token: ${err.message}`); 85 | } 86 | } 87 | } 88 | 89 | async function submitDocument(docs, token) { 90 | try { 91 | const payload = { 92 | documents: docs 93 | }; 94 | 95 | const response = await axios.post(`${process.env.PREPROD_BASE_URL}/api/v1.0/documentsubmissions`, payload, { 96 | headers: { 97 | 'Content-Type': 'application/json', 98 | 'Authorization': `Bearer ${token}` 99 | } 100 | }); 101 | 102 | return { status: 'success', data: response.data }; 103 | } catch (err) { 104 | if (err.response.status == 429) { 105 | const rateLimitReset = err.response.headers["x-rate-limit-reset"]; 106 | if (rateLimitReset) { 107 | const resetTime = new Date(rateLimitReset).getTime(); 108 | const currentTime = Date.now(); 109 | const waitTime = resetTime - currentTime; 110 | 111 | console.log('======================================================================================='); 112 | console.log(' LHDN SubmitDocument API hitting rate limit HTTP 429 '); 113 | console.log(' Retrying for current iteration................. '); 114 | console.log(` (Waiting time: ${waitTime} ms) `); 115 | console.log('======================================================================================='); 116 | 117 | if (waitTime > 0) { 118 | await new Promise(resolve => setTimeout(resolve, waitTime)); 119 | return await submitDocument(docs, token) 120 | } 121 | } 122 | } 123 | 124 | if (err.response.status == 500) { 125 | throw new Error('External LHDN SubmitDocument API hitting 500 (Internal Server Error). Please contact LHDN support.') 126 | } 127 | 128 | if (err.response.status == 400){ 129 | return { status: 'failed', error: err.response.data }; 130 | } else { 131 | return { status: 'failed', error: err.response.data };; 132 | } 133 | } 134 | } 135 | 136 | async function getDocumentDetails(irb_uuid, token) { 137 | try { 138 | const response = await axios.get(`${process.env.PREPROD_BASE_URL}/api/v1.0/documents/${irb_uuid}/details`, { 139 | headers: { 140 | // 'Content-Type': 'application/json', 141 | 'Authorization': `Bearer ${token}` 142 | } 143 | }); 144 | 145 | return { status: 'success', data: response.data }; 146 | } catch (err) { 147 | if (err.response.status == 429) { 148 | const rateLimitReset = err.response.headers["x-rate-limit-reset"]; 149 | if (rateLimitReset) { 150 | const resetTime = new Date(rateLimitReset).getTime(); 151 | const currentTime = Date.now(); 152 | const waitTime = resetTime - currentTime; 153 | 154 | console.log('======================================================================================='); 155 | console.log(' LHDN DocumentDetails API hitting rate limit HTTP 429 '); 156 | console.log(' Retrying for current iteration................. '); 157 | console.log(` (Waiting time: ${waitTime} ms) `); 158 | console.log('======================================================================================='); 159 | 160 | if (waitTime > 0) { 161 | await new Promise(resolve => setTimeout(resolve, waitTime)); 162 | return await getDocumentDetails(docs, token) 163 | } 164 | } 165 | } else { 166 | // throw new Error(`Failed to get IRB document details for document UUID ${irb_uuid}: ${err.message}`); 167 | console.error(`Failed to get IRB document details for document UUID ${irb_uuid}:`, err.message); 168 | throw err; 169 | } 170 | } 171 | } 172 | 173 | async function cancelValidDocumentBySupplier(irb_uuid, cancellation_reason, token) { 174 | let payload = { 175 | status: 'cancelled', 176 | reason: cancellation_reason ? cancellation_reason : 'NA' 177 | } 178 | 179 | try { 180 | const response = await axios.put(`${process.env.PREPROD_BASE_URL}/api/v1.0/documents/state/${irb_uuid}/state`, 181 | payload, 182 | { 183 | headers: { 184 | 'Content-Type': 'application/json', 185 | 'Authorization': `Bearer ${token}` 186 | } 187 | } 188 | ); 189 | 190 | return { status: 'success', data: response.data }; 191 | } catch (err) { 192 | if (err.response.status == 429) { 193 | const rateLimitReset = err.response.headers["x-rate-limit-reset"]; 194 | if (rateLimitReset) { 195 | const resetTime = new Date(rateLimitReset).getTime(); 196 | const currentTime = Date.now(); 197 | const waitTime = resetTime - currentTime; 198 | 199 | console.log('======================================================================================='); 200 | console.log(' LHDN Cancel Document API hitting rate limit HTTP 429 '); 201 | console.log(' Retrying for current iteration................. '); 202 | console.log(` (Waiting time: ${waitTime} ms) `); 203 | console.log('======================================================================================='); 204 | 205 | if (waitTime > 0) { 206 | await new Promise(resolve => setTimeout(resolve, waitTime)); 207 | return await cancelValidDocumentBySupplier(docs, token) 208 | } 209 | } 210 | } else { 211 | // throw new Error(`Failed to get IRB document details for document UUID ${irb_uuid}: ${err.message}`); 212 | console.error(`Failed to cancel document for IRB UUID ${irb_uuid}:`, err.message); 213 | throw err; 214 | } 215 | } 216 | } 217 | 218 | function jsonToBase64(jsonObj) { 219 | const jsonString = JSON.stringify(jsonObj); 220 | const base64String = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(jsonString)); 221 | return base64String; 222 | } 223 | 224 | function calculateSHA256(jsonObj) { 225 | const jsonString = JSON.stringify(jsonObj); 226 | const hash = CryptoJS.SHA256(jsonString); 227 | return hash.toString(CryptoJS.enc.Hex); 228 | } 229 | 230 | function getCertificatesHashedParams(documentJson) { 231 | //Note: Supply your JSON without Signature and UBLExtensions 232 | let jsonStringifyData = JSON.stringify(documentJson) 233 | const minifiedJsonData = jsonminify(jsonStringifyData); 234 | 235 | const sha256Hash = crypto.createHash('sha256').update(minifiedJsonData, 'utf8').digest('base64'); 236 | const docDigest = sha256Hash; 237 | 238 | const privateKeyPath = path.join(__dirname, 'eInvoiceCertificates', process.env.PRIVATE_KEY_FILE_PATH); 239 | const certificatePath = path.join(__dirname, 'eInvoiceCertificates', process.env.PRIVATE_CERT_FILE_PATH); 240 | 241 | const privateKeyPem = fs.readFileSync(privateKeyPath, 'utf8'); 242 | const certificatePem = fs.readFileSync(certificatePath, 'utf8'); 243 | 244 | const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); 245 | 246 | const md = forge.md.sha256.create(); 247 | //NOTE DEV: 12/7/2024 - sign the raw json instead of hashed json 248 | // md.update(docDigest, 'utf8'); //disable this (no longer work) 249 | md.update(minifiedJsonData, 'utf8'); //enable this 250 | const signature = privateKey.sign(md); 251 | const signatureBase64 = forge.util.encode64(signature); 252 | 253 | // ============================================================= 254 | // Calculate cert Digest 255 | // ============================================================= 256 | const certificate = forge.pki.certificateFromPem(certificatePem); 257 | const derBytes = forge.asn1.toDer(forge.pki.certificateToAsn1(certificate)).getBytes(); 258 | 259 | const sha256 = crypto.createHash('sha256').update(derBytes, 'binary').digest('base64'); 260 | const certDigest = sha256; 261 | 262 | // ============================================================= 263 | // Calculate the signed properties section digest 264 | // ============================================================= 265 | let signingTime = new Date().toISOString() 266 | let signedProperties = 267 | { 268 | "Target": "signature", 269 | "SignedProperties": [ 270 | { 271 | "Id": "id-xades-signed-props", 272 | "SignedSignatureProperties": [ 273 | { 274 | "SigningTime": [ 275 | { 276 | "_": signingTime 277 | } 278 | ], 279 | "SigningCertificate": [ 280 | { 281 | "Cert": [ 282 | { 283 | "CertDigest": [ 284 | { 285 | "DigestMethod": [ 286 | { 287 | "_": "", 288 | "Algorithm": "http://www.w3.org/2001/04/xmlenc#sha256" 289 | } 290 | ], 291 | "DigestValue": [ 292 | { 293 | "_": certDigest 294 | } 295 | ] 296 | } 297 | ], 298 | "IssuerSerial": [ 299 | { 300 | "X509IssuerName": [ 301 | { 302 | "_": process.env.X509IssuerName_VALUE 303 | } 304 | ], 305 | "X509SerialNumber": [ 306 | { 307 | "_": process.env.X509SerialNumber_VALUE 308 | } 309 | ] 310 | } 311 | ] 312 | } 313 | ] 314 | } 315 | ] 316 | } 317 | ] 318 | } 319 | ] 320 | } 321 | 322 | const signedpropsString = JSON.stringify(signedProperties); 323 | const signedpropsHash = crypto.createHash('sha256').update(signedpropsString, 'utf8').digest('base64'); 324 | 325 | // return ({ 326 | // docDigest, // docDigest 327 | // signatureBase64, // sig, 328 | // certDigest, 329 | // signedpropsHash, // propsDigest 330 | // signingTime 331 | // }) 332 | 333 | let certificateJsonPortion_Signature = [ 334 | { 335 | "ID": [ 336 | { 337 | "_": "urn:oasis:names:specification:ubl:signature:Invoice" 338 | } 339 | ], 340 | "SignatureMethod": [ 341 | { 342 | "_": "urn:oasis:names:specification:ubl:dsig:enveloped:xades" 343 | } 344 | ] 345 | } 346 | ] 347 | 348 | let certificateJsonPortion_UBLExtensions = [ 349 | { 350 | "UBLExtension": [ 351 | { 352 | "ExtensionURI": [ 353 | { 354 | "_": "urn:oasis:names:specification:ubl:dsig:enveloped:xades" 355 | } 356 | ], 357 | "ExtensionContent": [ 358 | { 359 | "UBLDocumentSignatures": [ 360 | { 361 | "SignatureInformation": [ 362 | { 363 | "ID": [ 364 | { 365 | "_": "urn:oasis:names:specification:ubl:signature:1" 366 | } 367 | ], 368 | "ReferencedSignatureID": [ 369 | { 370 | "_": "urn:oasis:names:specification:ubl:signature:Invoice" 371 | } 372 | ], 373 | "Signature": [ 374 | { 375 | "Id": "signature", 376 | "SignedInfo": [ 377 | { 378 | "SignatureMethod": [ 379 | { 380 | "_": "", 381 | "Algorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" 382 | } 383 | ], 384 | "Reference": [ 385 | { 386 | "Id": "id-doc-signed-data", 387 | "URI": "", 388 | "DigestMethod": [ 389 | { 390 | "_": "", 391 | "Algorithm": "http://www.w3.org/2001/04/xmlenc#sha256" 392 | } 393 | ], 394 | "DigestValue": [ 395 | { 396 | "_": docDigest 397 | } 398 | ] 399 | }, 400 | { 401 | "Id": "id-xades-signed-props", 402 | "Type": "http://uri.etsi.org/01903/v1.3.2#SignedProperties", 403 | "URI": "#id-xades-signed-props", 404 | "DigestMethod": [ 405 | { 406 | "_": "", 407 | "Algorithm": "http://www.w3.org/2001/04/xmlenc#sha256" 408 | } 409 | ], 410 | "DigestValue": [ 411 | { 412 | "_": signedpropsHash 413 | } 414 | ] 415 | } 416 | ] 417 | } 418 | ], 419 | "SignatureValue": [ 420 | { 421 | "_": signatureBase64 422 | } 423 | ], 424 | "KeyInfo": [ 425 | { 426 | "X509Data": [ 427 | { 428 | "X509Certificate": [ 429 | { 430 | "_": process.env.X509Certificate_VALUE 431 | } 432 | ], 433 | "X509SubjectName": [ 434 | { 435 | "_": process.env.X509SubjectName_VALUE 436 | } 437 | ], 438 | "X509IssuerSerial": [ 439 | { 440 | "X509IssuerName": [ 441 | { 442 | "_": process.env.X509IssuerName_VALUE 443 | } 444 | ], 445 | "X509SerialNumber": [ 446 | { 447 | "_": process.env.X509SerialNumber_VALUE 448 | } 449 | ] 450 | } 451 | ] 452 | } 453 | ] 454 | } 455 | ], 456 | "Object": [ 457 | { 458 | "QualifyingProperties": [ 459 | { 460 | "Target": "signature", 461 | "SignedProperties": [ 462 | { 463 | "Id": "id-xades-signed-props", 464 | "SignedSignatureProperties": [ 465 | { 466 | "SigningTime": [ 467 | { 468 | "_": signingTime 469 | } 470 | ], 471 | "SigningCertificate": [ 472 | { 473 | "Cert": [ 474 | { 475 | "CertDigest": [ 476 | { 477 | "DigestMethod": [ 478 | { 479 | "_": "", 480 | "Algorithm": "http://www.w3.org/2001/04/xmlenc#sha256" 481 | } 482 | ], 483 | "DigestValue": [ 484 | { 485 | "_": certDigest 486 | } 487 | ] 488 | } 489 | ], 490 | "IssuerSerial": [ 491 | { 492 | "X509IssuerName": [ 493 | { 494 | "_": process.env.X509IssuerName_VALUE 495 | } 496 | ], 497 | "X509SerialNumber": [ 498 | { 499 | "_": process.env.X509SerialNumber_VALUE 500 | } 501 | ] 502 | } 503 | ] 504 | } 505 | ] 506 | } 507 | ] 508 | } 509 | ] 510 | } 511 | ] 512 | } 513 | ] 514 | } 515 | ] 516 | } 517 | ] 518 | } 519 | ] 520 | } 521 | ] 522 | } 523 | ] 524 | } 525 | ] 526 | } 527 | ] 528 | 529 | //Use this return value to inject back into your raw JSON Invoice[0] without Signature/UBLExtension earlier 530 | //Then, encode back to SHA256 and Base64 respectively for object value inside Submission Document payload. 531 | return ({ 532 | certificateJsonPortion_Signature, 533 | certificateJsonPortion_UBLExtensions 534 | }) 535 | 536 | } 537 | 538 | async function testIRBCall(data) { 539 | try { 540 | const response = await axios.post(`${process.env.PREPROD_BASE_URL}/connect/token`, httpOptions, { 541 | headers: { 542 | 'Content-Type': 'application/x-www-form-urlencoded' 543 | } 544 | }); 545 | 546 | if(response.status == 200) return response.data; 547 | } catch (err) { 548 | if (err.response.status == 429) { 549 | console.log('Current iteration hitting Rate Limit 429 of LHDN Taxpayer Token API, retrying...') 550 | const rateLimitReset = err.response.headers["x-rate-limit-reset"]; 551 | 552 | if (rateLimitReset) { 553 | const resetTime = new Date(rateLimitReset).getTime(); 554 | const currentTime = Date.now(); 555 | const waitTime = resetTime - currentTime; 556 | 557 | if (waitTime > 0) { 558 | console.log('======================================================================================='); 559 | console.log(' (TEST API CALL) LHDN Taxpayer Token API hitting rate limit HTTP 429 '); 560 | console.log(` Refetching................. (Waiting time: ${waitTime} ms) `); 561 | console.log('======================================================================================='); 562 | await new Promise(resolve => setTimeout(resolve, waitTime)); 563 | return await getTokenAsTaxPayer(); 564 | } 565 | } 566 | } else { 567 | throw new Error(`Failed to get token: ${err.message}`); 568 | } 569 | } 570 | } 571 | 572 | async function validateCustomerTin(tin, idType, idValue, token) { 573 | try { 574 | if (!['NRIC', 'BRN', 'PASSPORT', 'ARMY'].includes(idType)) { 575 | throw new Error(`Invalid ID type. Only 'NRIC', 'BRN', 'PASSPORT', 'ARMY' are allowed`); 576 | } 577 | 578 | const response = await axios.get(`${process.env.PREPROD_BASE_URL}/api/v1.0/taxpayer/validate/${tin}?idType=${idType}&idValue=${idValue}`, { 579 | headers: { 580 | 'Authorization': `Bearer ${token}` 581 | } 582 | }); 583 | 584 | if (response.status === 200) { 585 | return { status: 'success' }; 586 | } 587 | } catch (err) { 588 | if (err.response) { 589 | if (err.response.status === 429) { 590 | console.log('Current iteration hitting Rate Limit 429 of LHDN Validate TIN API, retrying...'); 591 | const rateLimitReset = err.response.headers["x-rate-limit-reset"]; 592 | 593 | if (rateLimitReset) { 594 | const resetTime = new Date(rateLimitReset).getTime(); 595 | const currentTime = Date.now(); 596 | const waitTime = resetTime - currentTime; 597 | 598 | if (waitTime > 0) { 599 | console.log('======================================================================================='); 600 | console.log(' LHDN Validate TIN API hitting rate limit HTTP 429 '); 601 | console.log(` Refetching................. (Waiting time: ${waitTime} ms) `); 602 | console.log('======================================================================================='); 603 | await new Promise(resolve => setTimeout(resolve, waitTime)); 604 | return await validateCustomerTin(tin, idType, idValue, token); 605 | } 606 | } 607 | } else if (err.response.status === 404) { 608 | throw new Error('Invalid TIN'); 609 | } else { 610 | throw new Error(`Failed to validate TIN: ${err.response.statusText}`); 611 | } 612 | } else { 613 | throw new Error(`Failed to validate TIN: ${err.message}`); 614 | } 615 | } 616 | } 617 | 618 | module.exports = { 619 | validateCustomerTin, 620 | testIRBCall, 621 | getTokenAsTaxPayer, 622 | getTokenAsIntermediary, 623 | submitDocument, 624 | cancelValidDocumentBySupplier, 625 | getDocumentDetails, 626 | jsonToBase64, 627 | calculateSHA256, 628 | getCertificatesHashedParams 629 | }; 630 | --------------------------------------------------------------------------------