nav-connect
Version:
Nav Online Invoice connection module
261 lines • 12 kB
JavaScript
import axios from "axios";
import { Buffer } from "node:buffer";
import { ApiRequestType, XsdSchemaName, validateXml, XmlValidationError } from "nav-osa-core";
import { NavApiError, NavResponseXmlValidationError, NavXmlValidationError, } from "./errors.js";
import { DEFAULT_HTTP_TIMEOUT_MS, validateNavApiConfig } from "./configValidator.js";
import { decodeExchangeToken } from "./crypto.js";
import { buildRequestOrThrow, handleAxiosError, parseApiResponse, } from "./errorMapping.js";
import { RequestQueue } from "./rateLimiter.js";
import { assertRangeWithinLimit, buildDigestChunks, parseDateTimeInterval } from "./dateRange.js";
import { createBasicOnlineInvoiceRequest, createManageAnnulmentRequest, createManageInvoiceRequest, } from "./requestBuilder.js";
const DEFAULT_THROTTLE_MS = 5000;
export class NavConnect {
_config;
_baseUrl;
_client;
_validateResponse;
_httpTimeoutMs;
_queue;
static create(config) {
return new NavConnect(config);
}
constructor(config) {
validateNavApiConfig(config);
this._config = config;
this._httpTimeoutMs = config.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
this._queue = new RequestQueue(config.minIntervalMs ?? 0);
this._validateResponse = config.validateResponse ?? false;
this._baseUrl = this._config.baseUrlOverride ?? (this._config.testSystem
? "https://api-test.onlineszamla.nav.gov.hu/invoiceService/v3"
: "https://api.onlineszamla.nav.gov.hu/invoiceService/v3");
this._client = axios.create({
baseURL: this._baseUrl,
headers: {
"Content-Type": "application/xml",
},
timeout: this._httpTimeoutMs,
});
this._client.interceptors.request.use((config) => {
this._queue.markRequestSent();
return config;
});
}
get taxNumber() {
return this._config.taxNumber;
}
get testSystem() {
return this._config.testSystem;
}
get technicalUser() {
return this._config.technicalUser.user;
}
/**
* Builds the request XML, posts it through the rate limiter, validates
* the response (unless disabled), parses it and checks the result
* field. All public operations share this single error-transformation
* chain.
*/
async sendRequest(endpoint, requestType, reqObj) {
return this._queue.enqueue(async () => {
const requestXml = await buildRequestOrThrow(requestType, reqObj);
try {
const response = await this._client.post(endpoint, requestXml);
const xmlValidationWarnings = this._validateResponse
? await this.validateResponseXml(response.data, XsdSchemaName.InvoiceApi)
: [];
const rootName = `${requestType.replace(/Request$/, "")}Response`;
const data = await parseApiResponse(response.data, rootName);
return {
data,
...(xmlValidationWarnings.length > 0 && { xmlValidationWarnings }),
};
}
catch (error) {
if (error instanceof NavApiError)
throw error;
if (error instanceof XmlValidationError) {
throw new NavResponseXmlValidationError(error.errors);
}
if (axios.isAxiosError(error))
await handleAxiosError(error, this._httpTimeoutMs);
throw new NavApiError(`${endpoint} failed`, error);
}
});
}
async queryInvoiceDigest(params) {
const { from, to } = parseDateTimeInterval(params.insDate, "queryInvoiceDigest");
assertRangeWithinLimit(from, to);
const reqObj = {
...createBasicOnlineInvoiceRequest(this._config),
page: params.page,
invoiceDirection: params.invoiceDirectionType,
invoiceQueryParams: {
mandatoryQueryParams: {
insDate: params.insDate,
},
},
};
return this.sendRequest("/queryInvoiceDigest", ApiRequestType.QueryInvoiceDigestRequest, reqObj);
}
async queryInvoiceDigestAll(params) {
const throttle = params.throttleMs ?? DEFAULT_THROTTLE_MS;
const { from, to } = parseDateTimeInterval(params.insDate, "queryInvoiceDigestAll");
const chunks = buildDigestChunks(from, to);
const allDigests = [];
for (let ci = 0; ci < chunks.length; ci++) {
const chunk = chunks[ci];
let currentPage = 1;
let availablePage = 1;
do {
if (ci > 0 || currentPage > 1) {
await RequestQueue.sleep(throttle);
}
const response = await this.queryInvoiceDigest({
page: currentPage,
invoiceDirectionType: params.invoiceDirectionType,
insDate: {
dateTimeFrom: chunk.from,
dateTimeTo: chunk.to,
},
});
const digestResultRaw = response.data.invoiceDigestResult;
const digestResult = Array.isArray(digestResultRaw) ? digestResultRaw[0] : digestResultRaw;
if (digestResult?.invoiceDigest) {
allDigests.push(...digestResult.invoiceDigest);
}
availablePage = digestResult?.availablePage ? parseInt(String(digestResult.availablePage), 10) : 0;
if (params.onProgress) {
params.onProgress({
currentChunk: ci + 1,
totalChunks: chunks.length,
currentPage,
availablePages: availablePage,
chunkFrom: chunk.from,
chunkTo: chunk.to,
digestsCollected: allDigests.length,
});
}
currentPage++;
} while (currentPage <= availablePage);
}
return allDigests;
}
buildInvoiceNumberQueryRequest(params) {
return {
...createBasicOnlineInvoiceRequest(this._config),
invoiceNumberQuery: {
invoiceNumber: params.invoiceNumber,
invoiceDirection: params.invoiceDirection,
supplierTaxNumber: params.supplierTaxNumber,
},
};
}
async queryInvoiceData(params) {
return this.sendRequest("/queryInvoiceData", ApiRequestType.QueryInvoiceDataRequest, this.buildInvoiceNumberQueryRequest(params));
}
async queryInvoiceCheck(params) {
return this.sendRequest("/queryInvoiceCheck", ApiRequestType.QueryInvoiceCheckRequest, this.buildInvoiceNumberQueryRequest(params));
}
async validateResponseXml(xml, schemaType) {
const result = await validateXml(xml, schemaType);
if (result.valid)
return [];
return result.errors.map((e) => `[Response XML validation] ${e}`);
}
validateSequentialIndices(items, label) {
if (!items || items.length === 0) {
throw new NavApiError(`${label} must contain at least one item`);
}
for (let i = 0; i < items.length; i++) {
const op = items[i];
if (typeof op.index !== "number" || !Number.isInteger(op.index) || op.index < 1 || op.index > 100) {
throw new NavApiError(`${label}[${i}].index must be an integer between 1 and 100, got ${JSON.stringify(op.index)}`);
}
if (i > 0 && op.index !== items[i - 1].index + 1) {
throw new NavApiError(`${label} indices must be strictly increasing without gaps. ` +
`Expected ${items[i - 1].index + 1}, got ${op.index}`);
}
}
}
async tokenExchange() {
const reqObj = {
...createBasicOnlineInvoiceRequest(this._config),
};
const { data } = await this.sendRequest("/tokenExchange", ApiRequestType.TokenExchangeRequest, reqObj);
if (!data.encodedExchangeToken) {
throw new NavApiError("TokenExchangeResponse is missing encodedExchangeToken");
}
return {
token: decodeExchangeToken(data.encodedExchangeToken, this._config.technicalUser.exchangeKey),
tokenValidityFrom: data.tokenValidityFrom,
tokenValidityTo: data.tokenValidityTo,
serverTimestamp: data.header?.timestamp,
};
}
async manageInvoice(params) {
const { invoiceOperation, compressedContent, skipXmlValidation, exchangeToken } = params;
this.validateSequentialIndices(invoiceOperation, "invoiceOperation");
if (compressedContent && !skipXmlValidation) {
throw new NavApiError("Compressed content cannot be validated. Decompress the invoice data before submission, or set skipXmlValidation to true if you are certain the compressed XML is valid.");
}
if (!skipXmlValidation) {
for (const op of invoiceOperation) {
const xml = Buffer.from(op.invoiceData, "base64").toString("utf8");
const result = await validateXml(xml, XsdSchemaName.Data);
if (!result.valid) {
throw new NavXmlValidationError(`invoiceData at index ${op.index} (ManageInvoiceRequest)`, result.errors);
}
}
}
const effectiveExchangeToken = exchangeToken ?? (await this.tokenExchange()).token;
const reqObj = createManageInvoiceRequest(this._config, {
invoiceOperation,
compressedContent,
exchangeToken: effectiveExchangeToken,
});
return this.sendRequest("/manageInvoice", ApiRequestType.ManageInvoiceRequest, reqObj);
}
async manageAnnulment(params) {
const { annulmentOperations, exchangeToken } = params;
this.validateSequentialIndices(annulmentOperations, "annulmentOperations");
const effectiveExchangeToken = exchangeToken ?? (await this.tokenExchange()).token;
const reqObj = createManageAnnulmentRequest(this._config, {
annulmentOperations,
exchangeToken: effectiveExchangeToken,
});
return this.sendRequest("/manageAnnulment", ApiRequestType.ManageAnnulmentRequest, reqObj);
}
async queryTransactionStatus(params) {
const reqObj = {
...createBasicOnlineInvoiceRequest(this._config),
transactionId: params.transactionId,
...(params.returnOriginalRequest !== undefined && { returnOriginalRequest: params.returnOriginalRequest }),
};
return this.sendRequest("/queryTransactionStatus", ApiRequestType.QueryTransactionStatusRequest, reqObj);
}
async queryTransactionList(params) {
const reqObj = {
...createBasicOnlineInvoiceRequest(this._config),
page: params.page,
insDate: params.insDate,
...(params.requestStatus && { requestStatus: params.requestStatus }),
};
return this.sendRequest("/queryTransactionList", ApiRequestType.QueryTransactionListRequest, reqObj);
}
async queryTaxpayer(params) {
const reqObj = {
...createBasicOnlineInvoiceRequest(this._config),
taxNumber: params.taxNumber,
};
return this.sendRequest("/queryTaxpayer", ApiRequestType.QueryTaxpayerRequest, reqObj);
}
async queryInvoiceChainDigest(params) {
const reqObj = {
...createBasicOnlineInvoiceRequest(this._config),
page: params.page,
invoiceChainQuery: params.invoiceChainQuery,
};
return this.sendRequest("/queryInvoiceChainDigest", ApiRequestType.QueryInvoiceChainDigestRequest, reqObj);
}
}
//# sourceMappingURL=client.js.map