ANAF e- Factura 类型Script SDK
用于与罗马尼亚ANAF e-Factura系统互动的综合性TypeScript SDK. 这个SDK提供OAuth 2.0认证,文档上传/下载,验证,UBL生成,以及公司数据查询能力.
特征
- OAuth 2. 0 认证: 使用 USB 符号支持完成 OAuth 流量
- 文件操作: 上传、状态检查和下载
- 信件管理: 列出并标出发票信息
- 验证: XML 验证和数字签名验证
- PDF 转换: 将 XML 发票转换为 PDF 格式
- UBL 生成: 创建符合规定的UBL 2.1 XML发票
- 收到发票分析: 将收到的 UBL 2.1 和 CII 文档读入打出的标题外加其行
- 公司数据查询:从公开的ANAF API中获取罗马尼亚公司详情
- 类型脚本: 完全类型安全和IntelliSense支持
安装
pnpm add @florinszilagyi/anaf-ts-sdk
快速启动
SDK主要分为四类:
1. 认证 - OAuth 2.0认证
import { AnafAuthenticator } from '@florinszilagyi/anaf-ts-sdk';
const auth = new AnafAuthenticator({
clientId: 'your-oauth-client-id',
clientSecret: 'your-oauth-client-secret',
redirectUri: 'https://your-app.com/oauth/callback',
});
// Get authorization URL (user will authenticate with USB token)
const authUrl = auth.getAuthorizationUrl();
console.log('Redirect user to:', authUrl);
// Exchange authorization code for tokens
const tokens = await auth.exchangeCodeForToken(authorizationCode);
console.log('Access token:', tokens.access_token);
// Refresh tokens when needed
const newTokens = await auth.refreshAccessToken(tokens.refresh_token);
2. AnafEfacturaClient - API操作系统
客户端用刷新符号构建一次并解决访问符号 自己,所以没有电话 需要象征性的争论.
import { AnafEfacturaClient } from '@florinszilagyi/anaf-ts-sdk';
const client = new AnafEfacturaClient(
{
vatNumber: 'RO12345678',
testMode: true, // Use test environment
refreshToken: tokens.refresh_token,
},
auth // the AnafAuthenticator from step 1
);
// Upload a document
const uploadResult = await client.uploadDocument(xmlContent, {
standard: 'UBL',
executare: true,
});
// Check upload status
const status = await client.getUploadStatus(uploadResult.indexIncarcare);
// Download processed document (ZIP archive: invoice + detached signature)
if (status.idDescarcare) {
const result = await client.downloadDocument(status.idDescarcare);
// Or unwrap the archive and get the invoice XML directly
const invoiceXml = await client.downloadDocumentXml(status.idDescarcare);
// …which is what you need to render the invoice as a PDF
const pdf = await client.convertXmlToPdf(invoiceXml, 'FACT1');
}
// List recent messages
const messages = await client.getMessages({
zile: 7, // Last 7 days
filtru: 'E', // Only errors
});
// Validate XML
const validation = await client.validateXml(xmlContent, 'FACT1');
// Convert XML to PDF
const pdfBuffer = await client.convertXmlToPdf(xmlContent, 'FACT1');
3. UblBuilder - UBL XML 生成器
import { UblBuilder } from '@florinszilagyi/anaf-ts-sdk';
const builder = new UblBuilder();
const xml = builder.generateInvoiceXml({
invoiceNumber: 'INV-2024-001',
issueDate: new Date(),
supplier: {
registrationName: 'Company SRL',
companyId: 'RO12345678',
vatNumber: 'RO12345678',
address: {
street: 'Str. Example 1',
city: 'Bucharest',
postalZone: '010101',
},
},
customer: {
registrationName: 'Customer SRL',
companyId: 'RO87654321',
address: {
street: 'Str. Customer 2',
city: 'Cluj-Napoca',
postalZone: '400001',
},
},
lines: [
{
description: 'Product/Service',
quantity: 1,
unitPrice: 100,
taxPercent: 19,
},
],
isSupplierVatPayer: true,
});
可选政党和联系人:
const xml = builder.generateInvoiceXml({
// ...the fields above
note: 'Delegat: Ion Exemplu', // BT-22
supplier: { /* ... */ contactName: 'Ana Exemplu' }, // BT-41
customer: { /* ... */ contactName: 'Ion Exemplu' }, // BT-56
// BG-11: only for a seller that has appointed a tax representative.
taxRepresentative: {
name: 'Reprezentant Exemplu SRL',
vatNumber: 'RO11223344', // with country prefix
address: { street: 'Str. Exemplu 5', city: 'Cluj-Napoca', postalZone: '400002', county: 'RO-CJ' },
},
});
建筑师投出 AnafValidationError 当税务代表没有
名称,没有前缀的增值税号码,没有ISO 3166-2:RO县代码(RO-CJ用于
罗马尼亚语地址,或者当供应商不是增值税支付人时.
4. AnafDetailsClient - 公司数据查询
import { AnafDetailsClient } from '@florinszilagyi/anaf-ts-sdk';
const detailsClient = new AnafDetailsClient({
timeout: 30000,
url: 'https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva', // Optional: custom ANAF API URL
});
// Fetch company data by VAT code (single company)
const result = await detailsClient.getCompanyData('RO12345678');
if (result.success) {
console.log('Company:', result.data[0].name);
console.log('Address:', result.data[0].address);
console.log('VAT registered:', result.data[0].scpTva);
console.log('Registration number:', result.data[0].registrationNumber);
console.log('Phone:', result.data[0].contactPhone);
} else {
console.error('Error:', result.error);
}
// Validate VAT code format
const isValid = await detailsClient.isValidVatCode('RO12345678');
console.log('Valid format:', isValid);
// Batch fetch multiple companies (single API call)
const batchResult = await detailsClient.batchGetCompanyData(['RO12345678', 'RO87654321', 'RO11111111']);
if (batchResult.success) {
batchResult.data.forEach((company, index) => {
console.log(`Company ${index + 1}:`, company.name);
console.log(`VAT registered:`, company.scpTva);
});
} else {
console.error('Batch error:', batchResult.error);
}
// Bulk lookup: max 100 CUIs, a fixed request date, no e-Factura registry request
const bulk = await detailsClient.batchGetCompanyData(['12345678', '87654321'], {
date: '2026-01-31',
includeEfacturaRegistry: false,
});
if (bulk.success) {
bulk.data.forEach((company) => console.log(company.vatCode, company.caenCode));
console.log('Not found:', bulk.notFound);
} else if (bulk.retryable) {
console.log('Retry later', bulk.statusCode, bulk.retryAfter);
} else {
console.log('Final answer:', bulk.error, bulk.notFound);
}
// Configuration options
const customClient = new AnafDetailsClient({
timeout: 60000, // 60 second timeout
url: 'https://custom-anaf-proxy.example.com/api/tva', // Custom endpoint (e.g., proxy server)
});
5. 分析 发票 -- -- 收到阅读发票
分析通过SPV收到的文件——UBL 2.1 Invoice编号 :CreditNote 或 CII 类型
CrossIndustryInvoice ——入平头外加其源行. 每个数额
以十进制的形式回来 字符串 所发表的文件,从未
JavaScript 编号, 因此在它到达前没有丢失任何浮标
你自己的钱处理.
import { parseReceivedInvoice } from '@florinszilagyi/anaf-ts-sdk';
const xml = await client.downloadDocumentXml(status.idDescarcare);
const invoice = parseReceivedInvoice(xml);
invoice.documentStandard; // 'ubl' | 'cii'
invoice.documentKind; // 'invoice' | 'credit_note' | 'corrective' | a raw type code
invoice.pdfStandard; // 'FACT1' | 'FCN' — which converter convertXmlToPdf needs
invoice.invoiceNumber; // 'EXMPL-2026-001'
invoice.issueDate; // '2026-08-01'
invoice.supplierName; // 'Exemplu SRL'
invoice.supplierVatCode; // 'RO12345678'
invoice.totalAmount; // '119.00' — a string, not 119
invoice.notes; // every invoice note (BT-22) in document order; [] when there are none
invoice.supplierContact; // { name, telephone, email } | null
invoice.customerContact; // { name, telephone, email } | null
invoice.taxRepresentative; // { name, vatCode, address } | null (BG-11)
invoice.sourceLines; // one ReceivedInvoiceLine per document line
invoice.sourceLineDiagnostics; // human-readable notes for malformed/absent line facts
每个信头字段均为无效. invoiceNumber, (中文(简体) ), issueDate, (中文(简体) ),
totalAmount 剩下的就回来 null 当文档没有时
以不验证的形式出版或出版它们 —— 解析器
从不取代默认。 检查您自己的流量所依赖的字段
使用它们.
剖析者拒绝猜测. 它抛出:
AnafValidationError——文件超大(12 MB of UTF-8)或其 根元素不是一个公认的发票根.AnafXmlParsingError—— XML 结构不完善, 声明外部 实体在其DTD中(甚至一个未提及的实体),或 将一个元素命名为一个元素 保留 JavaScript 属性名称, 例如__proto__。 。 。 .AnafAmbiguousTaxTotalError文件载有若干增值税总额和 其中没有一个与文档货币相匹配.
字段不能读取为有效的小数、日期或货币返回为 null
而不是强迫, 和无法读取的线性事实报告
sourceLineDiagnostics 而线本身被保留.
普通和DTD实体扩展仍被禁用(这些文件是:
供应商授权,一个递归实体定义将气球内存).
五个预定义的 XML 实体和小数/hex 字符引用——
罗马尼亚的二极论者通常到达时
& 是文字文本 &永远 &。 。 。 .
AnafDetails 客户端配置
这个 AnafDetailsClient 支持以下配置选项:
| 选项 | 类型 | 默认 | 说明 |
|---|---|---|---|
timeout | number | 30000 | 请求超时( 毫秒) |
url | string | 'https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva' | ANAF API 端点 URL |
配置示例
// Default configuration
const client = new AnafDetailsClient();
// Custom timeout
const clientWithTimeout = new AnafDetailsClient({
timeout: 60000, // 60 seconds
});
// Custom API endpoint (useful for proxy servers or testing)
const clientWithCustomUrl = new AnafDetailsClient({
url: 'https://your-proxy.example.com/anaf-api',
timeout: 45000,
});
// Minimal configuration
const minimalClient = new AnafDetailsClient({
timeout: 15000, // Fast timeout for quick responses
});
自定义 URL 使用大小写
- 代理服务器: 通过您自己的登录/监测代理请求路由
- 装入平衡器: 在多个ANAF端点分发请求
- 测试: 开发期间指向模拟服务器
- 区域终点: 如果可用, 使用不同的 ANAF 区域服务器
- 公司防火墙:通过核准的公司网关的路线
完整示例
import { AnafAuthenticator, AnafClient, AnafDetailsClient, UblBuilder } from '@florinszilagyi/anaf-ts-sdk';
// Setup
const auth = new AnafAuthenticator({
clientId: process.env.ANAF_CLIENT_ID,
clientSecret: process.env.ANAF_CLIENT_SECRET,
redirectUri: 'https://myapp.com/oauth/callback',
});
const client = new AnafClient({
vatNumber: 'RO12345678',
testMode: true,
});
const detailsClient = new AnafDetailsClient();
const builder = new UblBuilder();
// 1. Get customer company data
const customerData = await detailsClient.getCompanyData('RO87654321');
if (!customerData.success) {
throw new Error(`Customer not found: ${customerData.error}`);
}
// 2. Authentication (one-time setup)
const authUrl = auth.getAuthorizationUrl();
// Direct user to authUrl, they authenticate with USB token
const tokens = await auth.exchangeCodeForToken(authCode);
// 3. Generate invoice XML using fetched company data
const xml = builder.generateInvoiceXml({
invoiceNumber: 'INV-2024-001',
issueDate: new Date(),
supplier: {
registrationName: 'My Company SRL',
companyId: 'RO12345678',
vatNumber: 'RO12345678',
address: {
street: 'Str. Example 1',
city: 'Bucharest',
postalZone: '010101',
},
},
customer: {
registrationName: customerData.data[0].name,
companyId: customerData.data[0].vatCode,
vatNumber: customerData.data[0].vatCode,
address: {
street: customerData.data[0].address,
city: 'Cluj-Napoca', // Parse from address if needed
postalZone: customerData.data[0].postalCode || '400001',
},
},
lines: [
{
description: 'Consulting Services',
quantity: 1,
unitPrice: 1000,
taxPercent: customerData.data[0].scpTva ? 19 : 0, // Apply VAT if customer is VAT registered
},
],
isSupplierVatPayer: true,
});
// 4. Upload to ANAF
const uploadResult = await client.uploadDocument(tokens.access_token, xml);
// 5. Monitor status
const status = await client.getUploadStatus(tokens.access_token, uploadResult.index_incarcare);
// 6. Download result
if (status.id_descarcare) {
const result = await client.downloadDocument(tokens.access_token, status.id_descarcare);
}
开发和测试
先决条件
-
USB 安全托肯: ANAF认证要求
- 支持的符号 : 罗马尼亚 CA 的任何合格证书
- 安装制造商驱动程序( 安全网、 Gemalto等)
- 证书必须在 ANAF SPV 注册
-
ANAF OAuth 应用程序登记地址: ANAF 门户
- 导航: Servicii Online →nregistrare utilizatori → DEZVOLTATORI APLICAȚII Î 二 (中文(简体) )
- 用您的回调 URL 注册应用程序
环境设置
创建一个 .env 在工程根中的文件 :
ANAF_CLIENT_ID=your_oauth_client_id_here
ANAF_CLIENT_SECRET=your_oauth_client_secret_here
本地开发与 ngrok
要进行本地测试, 您需要 OAuth 调用 URL :
-
安装 ngrok数字 :
# Using npm npm install -g ngrok # Or download from https://ngrok.com/ -
曝光本地服务器数字 :
# Start your local server on port 3000 npm start # In another terminal, expose it publicly ngrok http 3000 -
更新 OAuth 设置数字 :
- 复制 ngrok HTTPS URL(例如,
https://abc123.ngrok.io(中文(简体) ) - 注册回调 URL :
https://abc123.ngrok.io/oauth/callback - 更新您的 Anaf 认证器配置 :
const auth = new AnafAuthenticator({ clientId: process.env.ANAF_CLIENT_ID, clientSecret: process.env.ANAF_CLIENT_SECRET, redirectUri: 'https://abc123.ngrok.io/oauth/callback', // Your ngrok URL }); - 复制 ngrok HTTPS URL(例如,
OAuth 认证流程
OAuth 完整的 USB 令牌认证 :
-
生成授权 URL数字 :
const authUrl = auth.getAuthorizationUrl(); console.log('Direct user to:', authUrl); -
用户认证进程数字 :
- 用户点击/访问授权 URL
- ANAF 登录页面打开
- 插入 USB 键:用户插入 USB 安全标志
- 输入 PIN: 用户在提示时输入符号 PIN
- 证书选择: 浏览器显示证书选择对话框
- 选择证书: 用户选择适当的证书
- 授权应用程序: 用户授予您的应用程序权限
- 重定向: 浏览器以授权代码重定向到您的回调 URL
-
处理召回数字 :
// Your callback endpoint receives: ?code=AUTH_CODE&state=STATE app.get('/oauth/callback', async (req, res) => { const { code } = req.query; try { const tokens = await auth.exchangeCodeForToken(code); // Store tokens securely res.send('Authentication successful!'); } catch (error) { res.status(400).send('Authentication failed'); } }); -
使用访问托肯数字 :
// Token is valid for 1 hour const client = new AnafClient({ vatNumber: 'RO12345678' }); const result = await client.uploadDocument(tokens.access_token, xmlContent); -
刷新鼠标数字 :
// Refresh before expiration const newTokens = await auth.refreshAccessToken(tokens.refresh_token);
自动测试
SDK包括一个综合OAuth流的综合Jest测试:
# Run all tests
pnpm test
# Run OAuth authentication tests with callback server
pnpm test:auth
# Run tests with coverage
pnpm test:coverage
手动操作测试
测试套件包括一个有用的OAuth测试流程:
-
开始测试数字 :
pnpm test:auth -
回调服务器: 自动启动
http://localhost:4040 -
获取 OAuth URL:测试在控制台显示授权 URL
-
完成 OAuth 任务数字 :
- 复制 URL 到浏览器
- 提示时插入 USB 令牌
- 输入 PIN 并选择证书
- 授权应用程序
- 浏览器重定向到
localhost:4040/callback
-
自动按键处理: 测试抓取代码并交换令牌
测试环境
- 测试环境: 所有测试都使用ANAF测试环境
- OAuth 终点数字 :
logincert.anaf.ro - API 终点数字 :
api.anaf.ro/test - 回调 URL数字 :
http://localhost:4040/callback(用于测试)
托肯管理
- 自动保存到
token.secret测试期间 - 1小时后失效
- 刷新符号具有更长的有效性
- 测试自动刷新过期的符号
- 无效的令牌被自动清理
解决问题
USB 托肯问题
❌ Certificate selection failed
解决方案数字 :
- 确保正确插入 USB 令牌
- 安装制造商驱动程序
- 尝试不同的浏览器( 推荐 Chrome)
- 在浏览器设置中检查证书的有效性
OAuth 回召问题
❌ Redirect URI mismatch
解决方案数字 :
- 完全验证已注册的 URL 匹配
- 包含协议( https://) 和路径
- 对 ngrok : 使用 HTTPS URL, 而非 HTTP
- 检查后面的鞭打
网络问题
❌ Connection refused or timeout
解决方案数字 :
- 检查互联网连接
- 校验防火墙设置
- Ngrok: 确保隧道活动
- 尝试不同的 ngrok 区域 :
ngrok http 3000 --region eu
托肯过期
❌ Access token expired
解决方案数字 :
- 使用刷新令牌获取新访问令牌
- 在您的app中执行自动刷新令牌
- 存储标志过期时间并主动刷新
API 覆盖范围
SDK执行来自ANAF e-Factura OpenAPI规格的所有端点:
认证
- OAuth 2.0 授权流量
- 切换和刷新
文件操作
- QQ 上传文档 (
/upload, (中文(简体) ),/uploadb2c(中文(简体) ) - 请检查上传状态 (X)
/stareMesaj(中文(简体) ) - 下载已处理的文件(
/descarcare(中文(简体) )
信件管理
- QQ 以页标列出信件( L)
/listaMesajePaginatieFactura(中文(简体) ) - 列出最近的信件( L)
/listaMesajeFactura(中文(简体) )
验证和转换
- XML 验证
/validare/{standard}(中文(简体) ) - * 数字签字验证
/api/validate/signature(中文(简体) ) - XML 到 PDF 转换 (
/transformare/{standard}(中文(简体) ) - XML 到 PDF 不经过验证 (
/transformare/{standard}/DA(中文(简体) )
UBL 生成
- UBL 2.1 符合要求的XML 生成
- 罗马尼亚CIUS-RO规格支持
公司数据查询
- 按增值税代码获取公司数据
- 验证增值税代码格式
- QQ 批量抓取多个公司
- QQ 快取管理
环境配置
SDK支持测试和生产环境:
// Test environment (recommended for development)
const client = new AnafClient({
vatNumber: 'RO12345678',
testMode: true,
});
// Production environment
const client = new AnafClient({
vatNumber: 'RO12345678',
testMode: false,
});
处理错误
SDK为不同的情景提供了特定的出错类型:
import { AnafAuthenticationError, AnafValidationError, AnafApiError } from '@florinszilagyi/anaf-ts-sdk';
try {
await client.uploadDocument(token, xml);
} catch (error) {
if (error instanceof AnafAuthenticationError) {
// Handle authentication issues - refresh token or re-authenticate
console.log('Authentication failed:', error.message);
} else if (error instanceof AnafValidationError) {
// Handle validation errors - fix XML or parameters
console.log('Validation error:', error.message);
} else if (error instanceof AnafApiError) {
// Handle API errors - check status, retry, or contact support
console.log('API error:', error.message);
}
}
类型脚本支持
SDK用TypeScript来写,并提供了全面的类型定义:
import type { InvoiceInput, UploadStatus, ListMessagesResponse, ValidationResult, OAuthTokens } from '@florinszilagyi/anaf-ts-sdk';
安保最佳做法
- 永远不要交代页:1
token.secret财务报告和审定财务报表.env改为.gitignore - 使用 HTTPS: 在生产中总是使用 HTTPS 用于 OAuth 回调
- 验证证书: 确保 USB 令牌证书有效且不过期
- 安全信使存储: 安全存储令牌(加密、数据库、安全存储)
- 执行刷新: 过期前自动刷新符号
- 测试环境: 使用测试模式进行开发和中转
生产部署
部署到生产时:
-
注册生产 OAuth App数字 :
- 使用您的生产域调回 URL
- 获取单独的客户证书来制作
-
环境配置数字 :
const client = new AnafClient({ vatNumber: 'RO12345678', testMode: false, // Production mode }); -
安全召回处理数字 :
- 对所有 OAuth 回调使用 HTTPS
- 验证状态参数
- 实施CSRF保护
- 日志认证事件
-
托肯管理数字 :
- 安全存储令牌( 加密数据库)
- 执行自动刷新
- 处理刷新标志性过期
- 监视令牌的使用和过期
许可证
麻省理工学院许可证 - 详情见LICENSE文件.
捐款
- 叉子存储器
- 创建特性分支
- 做你的改变
- 添加测试
- 运行测试和起伏
- 提交拉动请求
支助
关于下列问题:
- ANAF e-Factura API: 检查 ANAF 正式文件
- 这个SDK: 在 GitHub 上打开一期
- OAuth 设置: 查阅ANAF SPV文件: ANAF 正式文件
- USB 托肯问题: 联系您的证书提供者
完美无缺:SaaS应用程序、会计软件、企业资源规划集成、发票系统
所需资源:USB安全标志,ANAF OAuth注册,Node.js 16+