viesapi-client
Version:
VIES API Client for JavaScript
731 lines (615 loc) • 20.7 kB
JavaScript
/**
* Copyright 2022-2025 NETCAT (www.netcat.pl)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author NETCAT <firma@netcat.pl>
* @copyright 2022-2025 NETCAT (www.netcat.pl)
* @license http://www.apache.org/licenses/LICENSE-2.0
*/
'use strict';
const maxios = require('axios').default;
const murl = require('url');
const mcrypto = require('crypto');
const mxmldom = require('@xmldom/xmldom');
const mxpath = require('xpath');
const mdatefns = require('date-fns');
const Err = require('./error');
const Number = require('./number');
const LegalForm = require('./legalform');
const NIP = require('./nip');
const EUVAT = require('./euvat');
const AccountStatus = require('./accountstatus');
const NameComponents = require('./namecomponents');
const AddressComponents = require('./addresscomponents');
const VIESData = require('./viesdata');
const VIESError = require('./vieserror');
const BatchResult = require('./batchresult');
VIESAPIClient.prototype.VERSION = '1.3.1';
VIESAPIClient.prototype.PRODUCTION_URL = 'https://viesapi.eu/api';
VIESAPIClient.prototype.TEST_URL = 'https://viesapi.eu/api-test';
VIESAPIClient.prototype.TEST_ID = 'test_id';
VIESAPIClient.prototype.TEST_KEY = 'test_key';
/**
* Construct new error object
* @constructor
* @param {int} code error code
* @param {string} description error description
*/
function ViesError(code, description)
{
this.message = (description + ' (code: ' + code + ')');
this.code = code;
this.description = description;
this.stack = Error().stack;
}
ViesError.prototype = Object.create(Error.prototype);
ViesError.prototype.constructor = ViesError;
/**
* Construct new service client object
* @param {string} id API key identifier
* @param {string} key API key
* @constructor
*/
function VIESAPIClient(id = undefined, key = undefined)
{
this.url = this.TEST_URL;
this.id = this.TEST_ID;
this.key = this.TEST_KEY;
if (id && key) {
this.url = this.PRODUCTION_URL;
this.id = id;
this.key = key;
}
this.app = '';
this.errcode = 0;
this.err = '';
}
/**
* Clear error
* @param {VIESAPIClient} viesapi client object
*/
function clear(viesapi)
{
viesapi.errcode = 0;
viesapi.err = '';
}
/**
* Set error details
* @param {VIESAPIClient} viesapi client object
* @param {int} code error code
* @param {string} err error message
*/
function set(viesapi, code, err = undefined)
{
viesapi.errcode = code;
viesapi.err = (err ? err : Err.message(code));
}
/**
* Get path suffix
* @param {VIESAPIClient} viesapi client object
* @param {number} type search number type as Number::xxx value
* @param {string} number search number value
* @return {string} path suffix
*/
function getPathSuffix(viesapi, type, number)
{
let path = undefined;
if (type === Number.NIP) {
if (!NIP.isValid(number)) {
set(viesapi, Err.CLI_NIP);
return undefined;
}
path = 'nip/' + NIP.normalize(number);
} else if (type === Number.EUVAT) {
if (!EUVAT.isValid(number)) {
set(viesapi, Err.CLI_EUVAT);
return undefined;
}
path = 'euvat/' + EUVAT.normalize(number);
}
return path;
}
/**
* Get element content as text
* @param {Document} doc XML document
* @param {string} path xpath string
* @return {string} element value
*/
function xpathString(doc, path)
{
const nodes = mxpath.select(path, doc);
if (!nodes) {
return '';
}
if (nodes.length !== 1) {
return '';
}
return nodes[0].toString().trim();
}
/**
* Get element content as integer
* @param {Document} doc XML document
* @param {string} path xpath string
* @return {number|undefined}
*/
function xpathInt(doc, path)
{
const val = xpathString(doc, path);
if (!val) {
return undefined;
}
return parseInt(val);
}
/**
* Get element content as float
* @param {Document} doc XML document
* @param {string} path xpath string
* @return {number|undefined}
*/
function xpathFloat(doc, path)
{
const val = xpathString(doc, path);
if (!val) {
return undefined;
}
return parseFloat(val);
}
/**
* Get element content as date in format yyyy-mm-dd
* @param {Document} doc XML document
* @param {string} path xpath string
* @return {Date} element value
*/
function xpathDate(doc, path)
{
const val = xpathString(doc, path);
if (!val) {
return undefined;
}
return mdatefns.parseISO(val.substring(0, 10));
}
/**
* Get element content as date in format yyyy-mm-dd hh:mm:ss
* @param {Document} doc XML document
* @param {string} path xpath string
* @return {Date} element value
*/
function xpathDateTime(doc, path)
{
const val = xpathString(doc, path);
if (!val) {
return undefined;
}
return mdatefns.parseISO(val);
}
/**
* Convert date to yyyy-mm-dd string
* @param {string|Date} date input date
* @return {string} output string
*/
function toString(date)
{
if (typeof date === 'string') {
// verify & format
const d = mdatefns.parse(date, 'yyyy-MM-dd', new Date());
return mdatefns.format(d, 'yyyy-MM-dd');
} else if (date && Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date)) {
// format
return mdatefns.format(date, 'yyyy-MM-dd');
}
return undefined;
}
/**
* Check if string is a valid guid
* @param {string} uuid string to check
* @return {boolean} true if string is a valid guid
*/
function isUuid(uuid)
{
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid);
}
/**
* Prepare authorization header content
* @param {VIESAPIClient} viesapi client object
* @param {string} method HTTP method
* @param {string} url target URL
* @return {string} authorization header content
*/
function auth(viesapi, method, url)
{
const u = murl.parse(url);
if (!u.port) {
u.port = u.protocol === 'https:' ? '443' : '80';
}
const nonce = mcrypto.randomBytes(4).toString('hex');
const ts = Math.round(Date.now() / 1000);
const str = "" + ts + "\n"
+ nonce + "\n"
+ method + "\n"
+ u.path + "\n"
+ u.hostname + "\n"
+ u.port + "\n"
+ "\n";
const mac = mcrypto.createHmac('sha256', viesapi.key).update(str).digest('base64');
return 'MAC id="' + viesapi.id + '", ts="' + ts + '", nonce="' + nonce + '", mac="' + mac + '"';
}
/**
* Prepare user agent information header content
* @param {VIESAPIClient} viesapi client object
* @return {string} user agent header content
*/
function userAgent(viesapi)
{
let ver = 'Unknown';
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
ver = 'Browser' + window.navigator.userAgent;
} else if (typeof process !== 'undefined' && process.versions != null && process.versions.node != null) {
ver = 'NodeJS ' + process.version;
}
return (viesapi.app ? viesapi.app + ' ' : '') + 'VIESAPIClient/' + viesapi.VERSION + ' Javascript/' + ver;
}
/**
* Parse XML response
* @param {VIESAPIClient} viesapi client object
* @param {any} data HTTP response
* @returns {Document|null} XML document or null
*/
function parseXml(viesapi, data)
{
try {
const doc = new mxmldom.DOMParser().parseFromString(data, 'text/xml');
const code = xpathString(doc, '/result/error/code/text()');
if (code) {
set(viesapi, parseInt(code), xpathString(doc, '/result/error/description/text()'));
return null;
}
return doc;
} catch (e) {
set(viesapi, Err.CLI_EXCEPTION, e.message);
}
return null;
}
/**
* Perform HTTP GET request
* @param {VIESAPIClient} viesapi client object
* @param {string} url target URL
* @return {Promise<Document>} promise returning XML document on success
*/
function get(viesapi, url)
{
const agent = userAgent(viesapi);
return maxios.get(url, {
headers: {
'Accept': 'text/xml',
'Authorization': auth(viesapi, 'GET', url),
'User-Agent': agent,
'X-VIESAPI-Client': agent
}
}).then(response => {
const doc = parseXml(viesapi, response.data);
if (!doc) {
throw new ViesError(viesapi.getLastErrorCode(), viesapi.getLastError());
}
return doc;
}).catch(e => {
if (!(e instanceof ViesError) && !parseXml(viesapi, e.response.data)) {
throw new ViesError(viesapi.getLastErrorCode(), viesapi.getLastError());
}
throw e;
});
}
/**
* Perform HTTP POST request
* @param {VIESAPIClient} viesapi client object
* @param {string} url target URL
* @param {string} type content type
* @param {string} content bytes
* @return {Promise<Document>} promise returning XML document on success
*/
function post(viesapi, url, type, content)
{
const agent = userAgent(viesapi);
return maxios.post(url, content, {
headers: {
'Accept': 'text/xml',
'Authorization': auth(viesapi, 'POST', url),
'Content-Type': type,
'User-Agent': agent,
'X-VIESAPI-Client': agent
}
}).then(response => {
const doc = parseXml(viesapi, response.data);
if (!doc) {
throw new ViesError(viesapi.getLastErrorCode(), viesapi.getLastError());
}
return doc;
}).catch(e => {
if (!(e instanceof ViesError) && !parseXml(viesapi, e.response.data)) {
throw new ViesError(viesapi.getLastErrorCode(), viesapi.getLastError());
}
throw e;
});
}
/**
* Set non default service URL
* @param {string} url service URL
*/
VIESAPIClient.prototype.setURL = function(url) {
this.url = url;
};
/**
* Set application info
* @param {string} app app info
*/
VIESAPIClient.prototype.setApp = function(app) {
this.app = app;
};
/**
* Get VIES data for specified number
* @param {string} euvat EU VAT number with 2-letter country prefix
* @return {Promise<VIESData>} promise returning VIES data on success
*/
VIESAPIClient.prototype.getVIESData = function(euvat) {
return new Promise((resolve, reject) => {
// clear error
clear(this);
// validate number and construct path
const suffix = getPathSuffix(this, Number.EUVAT, euvat);
if (!suffix) {
reject(new Error(this.getLastError()));
return;
}
// send request
get(this, this.url + '/get/vies/' + suffix).then((doc) => {
const vd = new VIESData();
vd.uid = xpathString(doc, '/result/vies/uid/text()');
vd.countryCode = xpathString(doc, '/result/vies/countryCode/text()');
vd.vatNumber = xpathString(doc, '/result/vies/vatNumber/text()');
vd.valid = (xpathString(doc, '/result/vies/valid/text()') === 'true');
vd.traderName = xpathString(doc, '/result/vies/traderName/text()');
vd.traderCompanyType = xpathString(doc, '/result/vies/traderCompanyType/text()');
vd.traderAddress = xpathString(doc, '/result/vies/traderAddress/text()');
vd.id = xpathString(doc, '/result/vies/id/text()');
vd.date = xpathDate(doc, '/result/vies/date/text()');
vd.source = xpathString(doc, '/result/vies/source/text()');
resolve(vd);
}).catch((e) => {
if (!(e instanceof ViesError)) {
set(this, Err.CLI_EXCEPTION, e.message);
}
reject(e);
});
});
};
/**
* Get VIES data returning parsed data for specified number
* @param {string} euvat EU VAT number with 2-letter country prefix
* @return {Promise<VIESData>} promise returning VIES data on success
*/
VIESAPIClient.prototype.getVIESDataParsed = function(euvat) {
return new Promise((resolve, reject) => {
// clear error
clear(this);
// validate number and construct path
const suffix = getPathSuffix(this, Number.EUVAT, euvat);
if (!suffix) {
reject(new Error(this.getLastError()));
return;
}
// send request
get(this, this.url + '/get/vies/parsed/' + suffix).then((doc) => {
const vd = new VIESData();
vd.uid = xpathString(doc, '/result/vies/uid/text()');
vd.countryCode = xpathString(doc, '/result/vies/countryCode/text()');
vd.vatNumber = xpathString(doc, '/result/vies/vatNumber/text()');
vd.valid = (xpathString(doc, '/result/vies/valid/text()') === 'true');
vd.traderName = xpathString(doc, '/result/vies/traderName/text()');
const name = xpathString(doc, '/result/vies/traderNameComponents/name/text()');
if (name) {
const nc = new NameComponents();
nc.name = name;
nc.legalForm = xpathString(doc, '/result/vies/traderNameComponents/legalForm/text()');
nc.legalFormCanonicalId = xpathInt(doc, '/result/vies/traderNameComponents/legalFormCanonicalId/text()');
nc.legalFormCanonicalName = xpathString(doc, '/result/vies/traderNameComponents/legalFormCanonicalName/text()');
vd.traderNameComponents = nc;
}
vd.traderCompanyType = xpathString(doc, '/result/vies/traderCompanyType/text()');
vd.traderAddress = xpathString(doc, '/result/vies/traderAddress/text()');
const country = xpathString(doc, '/result/vies/traderAddressComponents/country/text()');
if (country) {
const ac = new AddressComponents();
ac.country = country;
ac.postalCode = xpathString(doc, '/result/vies/traderAddressComponents/postalCode/text()');
ac.city = xpathString(doc, '/result/vies/traderAddressComponents/city/text()');
ac.street = xpathString(doc, '/result/vies/traderAddressComponents/street/text()');
ac.streetNumber = xpathString(doc, '/result/vies/traderAddressComponents/streetNumber/text()');
ac.houseNumber = xpathString(doc, '/result/vies/traderAddressComponents/houseNumber/text()');
vd.traderAddressComponents = ac;
}
vd.id = xpathString(doc, '/result/vies/id/text()');
vd.date = xpathDate(doc, '/result/vies/date/text()');
vd.source = xpathString(doc, '/result/vies/source/text()');
resolve(vd);
}).catch((e) => {
if (!(e instanceof ViesError)) {
set(this, Err.CLI_EXCEPTION, e.message);
}
reject(e);
});
});
};
/**
* Upload batch of VAT numbers and get their current VAT statuses and traders data
* @param {string[]} numbers Array of EU VAT numbers with 2-letter country prefix
* @return {Promise<string>} promise returning Batch token for checking status and getting the result
*/
VIESAPIClient.prototype.getVIESDataAsync = function(numbers) {
return new Promise((resolve, reject) => {
// clear error
clear(this);
// validate input
if (numbers.length < 2 || numbers.length > 99) {
set(this, Err.CLI_BATCH_SIZE);
reject(new Error(this.getLastError()));
return;
}
// prepare request
let xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
+ "<request>\r\n"
+ " <batch>\r\n"
+ " <numbers>\r\n";
for (const number of numbers) {
if (!EUVAT.isValid(number)) {
set(this, Err.CLI_EUVAT);
reject(new Error(this.getLastError()));
return;
}
xml += " <number>" + EUVAT.normalize(number) + "</number>\r\n";
}
xml += " </numbers>\r\n"
+ " </batch>\r\n"
+ "</request>";
// send request
post(this, this.url + '/batch/vies', 'text/xml; charset=UTF-8', xml).then((doc) => {
const token= xpathString(doc, '/result/batch/token/text()');
if (!token) {
set(this, Err.CLI_RESPONSE);
reject(new Error(this.getLastError()));
return;
}
resolve(token);
}).catch((e) => {
if (!(e instanceof ViesError)) {
set(this, Err.CLI_EXCEPTION, e.message);
}
reject(e);
});
});
};
/**
* Check batch result and download data
* @param {string} token Batch token received from getVIESDataAsync function
* @return {Promise<BatchResult>} promise returning batch result on success
*/
VIESAPIClient.prototype.getVIESDataAsyncResult = function(token) {
return new Promise((resolve, reject) => {
// clear error
clear(this);
// validate input
if (!token || !isUuid(token)) {
set(this, Err.CLI_INPUT);
reject(new Error(this.getLastError()));
return;
}
// send request
get(this, this.url + '/batch/vies/' + token).then((doc) => {
const br = new BatchResult();
for (let i = 1; ; i++) {
const uid = xpathString(doc, "/result/batch/numbers/vies[" + i + "]/uid/text()");
if (!uid) {
break;
}
const vd = new VIESData();
vd.uid = uid;
vd.countryCode = xpathString(doc, "/result/batch/numbers/vies[" + i + "]/countryCode/text()");
vd.vatNumber = xpathString(doc, "/result/batch/numbers/vies[" + i + "]/vatNumber/text()");
vd.valid = (xpathString(doc, "/result/batch/numbers/vies[" + i + "]/valid/text()") === "true");
vd.traderName = xpathString(doc, "/result/batch/numbers/vies[" + i + "]/traderName/text()");
vd.traderCompanyType = xpathString(doc, "/result/batch/numbers/vies[" + i + "]/traderCompanyType/text()");
vd.traderAddress = xpathString(doc, "/result/batch/numbers/vies[" + i + "]/traderAddress/text()");
vd.id = xpathString(doc, "/result/batch/numbers/vies[" + i + "]/id/text()");
vd.date = xpathDate(doc, "/result/batch/numbers/vies[" + i + "]/date/text()");
vd.source = xpathString(doc, "/result/batch/numbers/vies[" + i + "]/source/text()");
br.numbers.push(vd);
}
for (let i = 1; ; i++) {
const uid = xpathString(doc, "/result/batch/errors/error[" + i + "]/uid/text()");
if (!uid) {
break;
}
const ve = new VIESError();
ve.uid = uid;
ve.countryCode = xpathString(doc, "/result/batch/errors/error[" + i + "]/countryCode/text()");
ve.vatNumber = xpathString(doc, "/result/batch/errors/error[" + i + "]/vatNumber/text()");
ve.error = xpathString(doc, "/result/batch/errors/error[" + i + "]/error/text()");
ve.date = xpathDate(doc, "/result/batch/errors/error[" + i + "]/date/text()");
ve.source = xpathString(doc, "/result/batch/errors/error[" + i + "]/source/text()");
br.errors.push(ve);
}
resolve(br);
}).catch((e) => {
if (!(e instanceof ViesError)) {
set(this, Err.CLI_EXCEPTION, e.message);
}
reject(e);
});
});
};
/**
* Get current account status
* @return {Promise<AccountStatus>} promise returning account status on success
*/
VIESAPIClient.prototype.getAccountStatus = function() {
return new Promise((resolve, reject) => {
// clear error
clear(this);
// send request
get(this, this.url + '/check/account/status').then((doc) => {
const as = new AccountStatus();
as.uid = xpathString(doc, '/result/account/uid/text()');
as.type = xpathString(doc, '/result/account/type/text()');
as.validTo = xpathDateTime(doc, '/result/account/validTo/text()');
as.billingPlanName = xpathString(doc, '/result/account/billingPlan/name/text()');
as.subscriptionPrice = xpathFloat(doc, '/result/account/billingPlan/subscriptionPrice/text()');
as.itemPrice = xpathFloat(doc, '/result/account/billingPlan/itemPrice/text()');
as.itemPriceStatus = xpathFloat(doc, '/result/account/billingPlan/itemPriceCheckStatus/text()');
as.itemPriceParsed = xpathFloat(doc, '/result/account/billingPlan/itemPriceStatusParsed/text()');
as.limit = xpathInt(doc, '/result/account/billingPlan/limit/text()');
as.requestDelay = xpathInt(doc, '/result/account/billingPlan/requestDelay/text()');
as.domainLimit = xpathInt(doc, '/result/account/billingPlan/domainLimit/text()');
as.overPlanAllowed = (xpathString(doc, '/result/account/billingPlan/overplanAllowed/text()') === 'true');
as.excelAddIn = (xpathString(doc, '/result/account/billingPlan/excelAddin/text()') === 'true');
as.app = (xpathString(doc, '/result/account/billingPlan/app/text()') === 'true');
as.cli = (xpathString(doc, '/result/account/billingPlan/cli/text()') === 'true');
as.stats = (xpathString(doc, '/result/account/billingPlan/stats/text()') === 'true');
as.monitor = (xpathString(doc, '/result/account/billingPlan/monitor/text()') === 'true');
as.funcGetVIESData = (xpathString(doc, '/result/account/billingPlan/funcGetVIESData/text()') === 'true');
as.funcGetVIESDataParsed = (xpathString(doc, '/result/account/billingPlan/funcGetVIESDataParsed/text()') === 'true');
as.viesDataCount = xpathInt(doc, '/result/account/requests/viesData/text()');
as.viesDataParsedCount = xpathInt(doc, '/result/account/requests/viesDataParsed/text()');
as.totalCount = xpathInt(doc, '/result/account/requests/total/text()');
resolve(as);
}).catch((e) => {
if (!(e instanceof ViesError)) {
set(this, Err.CLI_EXCEPTION, e.message);
}
reject(e);
});
});
};
/**
* Get last error code
* @returns {int} error code
*/
VIESAPIClient.prototype.getLastErrorCode = function () {
return this.errcode;
};
/**
* Get last error message
* @returns {string} error message
*/
VIESAPIClient.prototype.getLastError = function () {
return this.err;
};
module.exports = VIESAPIClient;