anaf-ts-sdk
Version:
Complete TypeScript SDK for Romanian ANAF API -E-Factura, Company checks
599 lines (598 loc) • 27.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnafEfacturaClient = void 0;
const errors_1 = require("./errors");
const constants_1 = require("./constants");
const xmlParser_1 = require("./utils/xmlParser");
const dateUtils_1 = require("./utils/dateUtils");
const httpClient_1 = require("./utils/httpClient");
const tryCatch_1 = require("./tryCatch");
/**
* Main client for interacting with ANAF e-Factura API
*
* This client handles automatic token management and all API operations.
* Both configuration and authenticator are required for initialization.
*
* @example
* ```typescript
* import { AnafEfacturaClient, AnafAuthenticator } from 'efactura-ts-sdk';
*
* // Create authenticator with OAuth credentials
* const authenticator = new AnafAuthenticator({
* clientId: 'your_client_id',
* clientSecret: 'your_client_secret',
* redirectUri: 'http://localhost:3000/callback',
* testMode: true
* });
*
* // Create client with config and authenticator (both required)
* const client = new AnafEfacturaClient({
* vatNumber: 'RO12345678',
* testMode: true,
* refreshToken: 'your_refresh_token' // obtained from OAuth flow
* }, authenticator);
*
* // Upload document (automatic token management)
* const uploadResult = await client.uploadDocument(xmlContent);
*
* // Check status (automatic token refresh if needed)
* const status = await client.getUploadStatus(uploadResult.indexIncarcare);
*
* // Download processed document
* if (status.stare === 'ok' && status.idDescarcare) {
* const document = await client.downloadDocument(status.idDescarcare);
* }
* ```
*/
class AnafEfacturaClient {
/**
* Create a new ANAF e-Factura client
*
* @param config Client configuration
* @param authenticator Authenticator for OAuth flows and token refresh
* @throws {AnafValidationError} If required configuration is missing
*/
constructor(config, authenticator) {
var _a, _b, _c, _d;
this.validateConfig(config);
this.config = Object.assign(Object.assign({}, config), { testMode: (_a = config.testMode) !== null && _a !== void 0 ? _a : false, timeout: (_b = config.timeout) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_TIMEOUT, axiosOptions: (_c = config.axiosOptions) !== null && _c !== void 0 ? _c : {}, basePath: (_d = config.basePath) !== null && _d !== void 0 ? _d : '', refreshToken: config.refreshToken });
this.basePath = this.config.basePath || (0, constants_1.getBasePath)('oauth', this.config.testMode);
this.httpClient = new httpClient_1.HttpClient({
baseURL: this.basePath,
timeout: this.config.timeout,
});
// Initialize authentication - both refreshToken and authenticator are now required
this.refreshToken = config.refreshToken;
this.authenticator = authenticator;
}
// ==========================================================================
// DOCUMENT UPLOAD
// ==========================================================================
/**
* Upload invoice document to ANAF
*
* Uploads an XML invoice document (UBL, CN, CII, or RASP format) to ANAF
* for processing in the e-Factura system.
*
* @param xmlContent XML document content as string
* @param options Upload options (standard, extern, etc.)
* @returns Upload status with upload ID for tracking
* @throws {AnafApiError} If upload fails
* @throws {AnafValidationError} If parameters are invalid
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async uploadDocument(xmlContent, options = {}) {
this.validateXmlContent(xmlContent);
this.validateUploadOptions(options);
const params = (0, constants_1.buildUploadParams)(this.config.vatNumber, options);
const url = `${constants_1.UPLOAD_PATH}?${params.toString()}`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.post(url, xmlContent, {
headers: {
'Content-Type': 'application/xml',
Authorization: `Bearer ${accessToken}`,
},
});
return (0, xmlParser_1.parseUploadResponse)(response.data);
});
if (error) {
this.handleApiError(error, 'Failed to upload document');
}
return data;
}
/**
* Upload B2C (Business to Consumer) invoice
*
* Simplified upload method for B2C invoices with reduced validation requirements.
* Uses identical parameters and response format as B2B upload.
*
* @param xmlContent XML document content as string
* @param options Upload options
* @returns Upload status with upload ID for tracking
* @throws {AnafApiError} If upload fails
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async uploadB2CDocument(xmlContent, options = {}) {
this.validateXmlContent(xmlContent);
this.validateUploadOptions(options);
const params = (0, constants_1.buildUploadParams)(this.config.vatNumber, options);
const url = `${constants_1.UPLOAD_B2C_PATH}?${params.toString()}`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.post(url, xmlContent, {
headers: {
'Content-Type': 'application/xml',
Authorization: `Bearer ${accessToken}`,
},
});
return (0, xmlParser_1.parseUploadResponse)(response.data);
});
if (error) {
this.handleApiError(error, 'Failed to upload B2C document');
}
return data;
}
// ==========================================================================
// STATUS AND DOWNLOAD
// ==========================================================================
/**
* Get upload status
*
* Check the processing status of a previously uploaded document.
*
* @param uploadId Upload ID returned from upload operation
* @returns Current status of the upload
* @throws {AnafApiError} If status check fails
* @throws {AnafValidationError} If parameters are invalid
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async getUploadStatus(uploadId) {
this.validateUploadId(uploadId);
const params = (0, constants_1.buildStatusParams)(uploadId);
const url = `${constants_1.STATUS_MESSAGE_PATH}?${params.toString()}`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return (0, xmlParser_1.parseStatusResponse)(response.data);
});
if (error) {
this.handleApiError(error, 'Failed to get upload status');
}
return data;
}
/**
* Download processed document
*
* Download the result of a processed document, which may include:
* - Validated and signed XML
* - Error details if processing failed
* - ZIP archive with multiple files
*
* @param downloadId Download ID from status response
* @returns Document content as string
* @throws {AnafApiError} If download fails
* @throws {AnafValidationError} If parameters are invalid
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async downloadDocument(downloadId) {
this.validateDownloadId(downloadId);
const params = (0, constants_1.buildDownloadParams)(downloadId);
const url = `${constants_1.DOWNLOAD_PATH}?${params.toString()}`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data;
});
if (error) {
this.handleApiError(error, 'Failed to download document');
}
return data;
}
// ==========================================================================
// MESSAGE LISTING
// ==========================================================================
/**
* Get messages with pagination
*
* Retrieve messages with pagination support for large result sets.
*
* @param params Paginated message parameters
* @returns List of messages for the specified page
* @throws {AnafApiError} If message retrieval fails
* @throws {AnafValidationError} If parameters are invalid
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async getMessagesPaginated(params) {
this.validatePaginatedMessagesParams(params);
const queryParams = (0, constants_1.buildPaginatedMessagesParams)(this.config.vatNumber, params.startTime, params.endTime, params.pagina, params.filtru);
const url = `${constants_1.LIST_MESSAGES_PAGINATED_PATH}?${queryParams.toString()}`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const data = (0, xmlParser_1.parseJsonResponse)(response.data);
if ((0, xmlParser_1.isErrorResponse)(data)) {
throw new errors_1.AnafApiError((0, xmlParser_1.extractErrorMessage)(data) || 'Error retrieving paginated messages');
}
return data;
});
if (error) {
this.handleApiError(error, 'Failed to get paginated messages');
}
return data;
}
/**
* Get recent messages
*
* Retrieve messages from ANAF for the configured VAT number within
* the specified number of days.
*
* @param params Message listing parameters
* @returns List of messages
* @throws {AnafApiError} If message retrieval fails
* @throws {AnafValidationError} If parameters are invalid
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async getMessages(params) {
this.validateListMessagesParams(params);
const queryParams = (0, constants_1.buildListMessagesParams)(this.config.vatNumber, params.zile, params.filtru);
const url = `${constants_1.LIST_MESSAGES_PATH}?${queryParams.toString()}`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const data = (0, xmlParser_1.parseJsonResponse)(response.data);
if ((0, xmlParser_1.isErrorResponse)(data)) {
throw new errors_1.AnafApiError((0, xmlParser_1.extractErrorMessage)(data) || 'Error retrieving messages');
}
return data;
});
if (error) {
this.handleApiError(error, 'Failed to get messages');
}
return data;
}
// ==========================================================================
// VALIDATION AND CONVERSION
// ==========================================================================
/**
* Validate XML document
*
* Validate an XML document against ANAF schemas without uploading it
* to the e-Factura system.
*
* @param xmlContent XML document to validate
* @param standard Document standard (FACT1 or FCN)
* @returns Validation result
* @throws {AnafApiError} If validation request fails
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async validateXml(xmlContent, standard = 'FACT1') {
this.validateXmlContent(xmlContent);
this.validateDocumentStandard(standard);
const url = `/validare/${standard}`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.post(url, xmlContent, {
headers: {
'Content-Type': 'text/plain',
Authorization: `Bearer ${accessToken}`,
},
});
const responseData = (0, xmlParser_1.parseJsonResponse)(response.data);
return {
valid: responseData.stare === 'ok',
details: responseData.Messages
? responseData.Messages.map((m) => m.message).join('\n')
: `Validation ${responseData.stare === 'ok' ? 'passed' : 'failed'}`,
info: `Validation performed using ${standard} standard (trace_id: ${responseData.trace_id})`,
};
});
if (error) {
this.handleApiError(error, 'Failed to validate XML');
}
return data;
}
/**
* Validate digital signature
*
* Validate the digital signature of an XML document and signature file.
* Accepts either File objects (browser) or Buffer objects (Node.js).
*
* @param xmlFile XML document file (File in browser, Buffer in Node.js)
* @param signatureFile Signature file (File in browser, Buffer in Node.js)
* @param xmlFileName Name for the XML file (required for Buffer uploads)
* @param signatureFileName Name for the signature file (required for Buffer uploads)
* @returns Validation result
* @throws {AnafApiError} If signature validation fails
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async validateSignature(xmlFile, signatureFile, xmlFileName, signatureFileName) {
const url = `/api/validate/signature`;
const formData = new FormData();
// Handle File objects (browser) vs Buffer objects (Node.js)
if (typeof File !== 'undefined' && xmlFile instanceof File) {
formData.append('file', xmlFile);
}
else if (xmlFile instanceof Buffer) {
if (!xmlFileName) {
throw new errors_1.AnafValidationError('XML file name is required when uploading Buffer');
}
// Create Blob-like object for Node.js compatibility
const blob = new Blob([xmlFile], { type: 'text/xml' });
formData.append('file', blob, xmlFileName);
}
else {
throw new errors_1.AnafValidationError('Invalid XML file type. Expected File or Buffer');
}
if (typeof File !== 'undefined' && signatureFile instanceof File) {
formData.append('signature', signatureFile);
}
else if (signatureFile instanceof Buffer) {
if (!signatureFileName) {
throw new errors_1.AnafValidationError('Signature file name is required when uploading Buffer');
}
// Create Blob-like object for Node.js compatibility
const blob = new Blob([signatureFile], { type: 'application/octet-stream' });
formData.append('signature', blob, signatureFileName);
}
else {
throw new errors_1.AnafValidationError('Invalid signature file type. Expected File or Buffer');
}
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.post(url, formData, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const responseData = response.data;
const msg = responseData.msg || '';
return {
valid: msg.includes('validate cu succes') && !msg.includes('NU'),
details: msg,
};
});
if (error) {
this.handleApiError(error, 'Failed to validate signature');
}
return data;
}
/**
* Convert XML to PDF with validation
*
* Convert an e-Factura XML document to PDF format with validation.
* According to the schema, this either returns PDF binary data or JSON error response.
*
* @param xmlContent XML document to convert
* @param standard Document standard (FACT1 or FCN)
* @returns PDF content as Buffer
* @throws {AnafApiError} If conversion fails
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async convertXmlToPdf(xmlContent, standard = 'FACT1') {
this.validateXmlContent(xmlContent);
this.validateDocumentStandard(standard);
const url = `/transformare/${standard}`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
var _a, _b;
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.post(url, xmlContent, {
headers: {
'Content-Type': 'text/plain',
Authorization: `Bearer ${accessToken}`,
},
});
// Check if response is JSON (error) or binary (PDF)
if ((_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get('content-type')) === null || _b === void 0 ? void 0 : _b.includes('application/json')) {
const errorData = (0, xmlParser_1.parseJsonResponse)(response.data);
throw new errors_1.AnafApiError(errorData.Messages ? errorData.Messages.map((m) => m.message).join('\n') : 'PDF conversion failed');
}
// Return PDF binary data
if (response.data instanceof ArrayBuffer) {
return Buffer.from(response.data);
}
else {
// Fallback for when content-type detection doesn't work
return Buffer.from(response.data);
}
});
if (error) {
this.handleApiError(error, 'Failed to convert XML to PDF');
}
return data;
}
/**
* Convert XML to PDF without validation
*
* Convert an e-Factura XML document to PDF format without validation.
* Note: Without validation, ANAF does not guarantee the correctness of the generated PDF.
*
* @param xmlContent XML document to convert
* @param standard Document standard (FACT1 or FCN)
* @returns PDF content as Buffer
* @throws {AnafApiError} If conversion fails
* @throws {AnafAuthenticationError} If authentication is not configured or fails
*/
async convertXmlToPdfNoValidation(xmlContent, standard = 'FACT1') {
this.validateXmlContent(xmlContent);
this.validateDocumentStandard(standard);
const url = `/transformare/${standard}/DA`;
const { data, error } = (0, tryCatch_1.tryCatch)(async () => {
var _a, _b;
const accessToken = await this.getValidAccessToken();
const response = await this.httpClient.post(url, xmlContent, {
headers: {
'Content-Type': 'text/plain',
Authorization: `Bearer ${accessToken}`,
},
});
// Check if response is JSON (error) or binary (PDF)
if ((_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get('content-type')) === null || _b === void 0 ? void 0 : _b.includes('application/json')) {
const errorData = (0, xmlParser_1.parseJsonResponse)(response.data);
throw new errors_1.AnafApiError(errorData.Messages ? errorData.Messages.map((m) => m.message).join('\n') : 'PDF conversion failed');
}
// Return PDF binary data
if (response.data instanceof ArrayBuffer) {
return Buffer.from(response.data);
}
else {
// Fallback for when content-type detection doesn't work
return Buffer.from(response.data);
}
});
if (error) {
this.handleApiError(error, 'Failed to convert XML to PDF without validation');
}
return data;
}
// ==========================================================================
// PRIVATE METHODS
// ==========================================================================
/**
* Get a valid access token, refreshing if necessary
* @returns A valid access token
* @throws {AnafAuthenticationError} If token refresh fails
*/
async getValidAccessToken() {
// Check if current token is still valid
if (this.isTokenValid()) {
return this.currentAccessToken;
}
// Refresh the token
await this.refreshAccessToken();
return this.currentAccessToken;
}
/**
* Check if the current access token is valid and not expired
* @returns True if token is valid and not expired
*/
isTokenValid() {
if (!this.currentAccessToken || !this.accessTokenExpiresAt) {
return false;
}
// Add 30 second buffer to avoid using tokens that are about to expire
const bufferMs = 30 * 1000;
return Date.now() < this.accessTokenExpiresAt - bufferMs;
}
/**
* Refresh the access token using the stored refresh token
* @throws {AnafAuthenticationError} If token refresh fails
*/
async refreshAccessToken() {
try {
const tokenResponse = await this.authenticator.refreshAccessToken(this.refreshToken);
this.currentAccessToken = tokenResponse.access_token;
this.accessTokenExpiresAt = Date.now() + tokenResponse.expires_in * 1000;
// Update refresh token if a new one was provided
if (tokenResponse.refresh_token) {
this.refreshToken = tokenResponse.refresh_token;
}
}
catch (error) {
throw new errors_1.AnafAuthenticationError(`Failed to refresh access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
validateConfig(config) {
var _a, _b;
if (!config) {
throw new errors_1.AnafValidationError('Configuration is required');
}
if (!((_a = config.vatNumber) === null || _a === void 0 ? void 0 : _a.trim())) {
throw new errors_1.AnafValidationError('VAT number is required');
}
if (!((_b = config.refreshToken) === null || _b === void 0 ? void 0 : _b.trim())) {
throw new errors_1.AnafValidationError('Refresh token is required for automatic authentication');
}
}
validateXmlContent(xmlContent) {
if (!(xmlContent === null || xmlContent === void 0 ? void 0 : xmlContent.trim())) {
throw new errors_1.AnafValidationError('XML content is required');
}
}
validateUploadId(uploadId) {
if (!(uploadId === null || uploadId === void 0 ? void 0 : uploadId.trim())) {
throw new errors_1.AnafValidationError('Upload ID is required');
}
}
validateDownloadId(downloadId) {
if (!(downloadId === null || downloadId === void 0 ? void 0 : downloadId.trim())) {
throw new errors_1.AnafValidationError('Download ID is required');
}
}
validateListMessagesParams(params) {
if (!params) {
throw new errors_1.AnafValidationError('Message listing parameters are required');
}
if (!(0, dateUtils_1.isValidDaysParameter)(params.zile)) {
throw new errors_1.AnafValidationError('Days parameter must be between 1 and 60');
}
}
validatePaginatedMessagesParams(params) {
if (!params) {
throw new errors_1.AnafValidationError('Paginated message parameters are required');
}
if (typeof params.startTime !== 'number' || params.startTime <= 0) {
throw new errors_1.AnafValidationError('Valid start time is required');
}
if (typeof params.endTime !== 'number' || params.endTime <= 0) {
throw new errors_1.AnafValidationError('Valid end time is required');
}
if (params.endTime <= params.startTime) {
throw new errors_1.AnafValidationError('End time must be after start time');
}
if (typeof params.pagina !== 'number' || params.pagina < 1) {
throw new errors_1.AnafValidationError('Page number must be 1 or greater');
}
}
validateUploadOptions(options) {
if (options.standard && !['UBL', 'CN', 'CII', 'RASP'].includes(options.standard)) {
throw new errors_1.AnafValidationError('Standard must be one of: UBL, CN, CII, RASP');
}
}
validateDocumentStandard(standard) {
if (!['FACT1', 'FCN'].includes(standard)) {
throw new errors_1.AnafValidationError('Document standard must be FACT1 or FCN');
}
}
handleApiError(error, context) {
var _a, _b, _c;
if (error instanceof errors_1.AnafSdkError) {
throw error;
}
else {
// Check if it's an HTTP error with status code
if (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) || (error === null || error === void 0 ? void 0 : error.status)) {
const status = ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) || error.status;
const errorMessage = error.message || ((_c = error.response) === null || _c === void 0 ? void 0 : _c.statusText) || 'Unknown error';
switch (status) {
case 400:
throw new errors_1.AnafValidationError(`${context}: Invalid request - ${errorMessage}`);
case 401:
throw new errors_1.AnafAuthenticationError(`${context}: Authentication failed - ${errorMessage}`);
case 500:
throw new errors_1.AnafApiError(`${context}: Server error - ${errorMessage}`, status);
default:
throw new errors_1.AnafApiError(`${context}: HTTP ${status} - ${errorMessage}`, status);
}
}
throw new errors_1.AnafSdkError(`${context}: ${error.message || 'Unknown error'}`);
}
}
}
exports.AnafEfacturaClient = AnafEfacturaClient;