UNPKG

nav-connect

Version:

Nav Online Invoice connection module

261 lines 12 kB
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