bee-coded / laravel-efactura-sdk
Laravel SDK for Romania's ANAF e-Factura electronic invoicing system
Requires
- php: ^8.4
- illuminate/contracts: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- sabre/xml: ^4.0
- spatie/laravel-data: ^4.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
README
A Laravel package for integrating with Romania's ANAF e-Factura (electronic invoicing) system.
Features
- OAuth 2.0 Authentication - Complete OAuth flow with JWT tokens and automatic token refresh
- Document Operations - Upload, download, and check status of invoices
- UBL 2.1 XML Generation - Generate CIUS-RO compliant invoice XML
- Company Lookup - Query ANAF for company details (VAT status, addresses, etc.)
- Validation - Validate XML against ANAF schemas before upload
- PDF Conversion - Convert XML invoices to PDF format
- Rate Limiting - Built-in protection against exceeding ANAF API quotas
Requirements
- PHP 8.4+
- Laravel 11.0+
- Valid ANAF OAuth credentials
Installation
composer require bee-coded/laravel-efactura-sdk
Publish the configuration file:
php artisan vendor:publish --tag=efactura-sdk-config
Configuration
Add the following to your .env file:
EFACTURA_SANDBOX=true EFACTURA_CLIENT_ID=your-client-id EFACTURA_CLIENT_SECRET=your-client-secret EFACTURA_REDIRECT_URI=https://your-app.com/efactura/callback
Configuration Options
// config/efactura-sdk.php return [ 'sandbox' => env('EFACTURA_SANDBOX', true), 'oauth' => [ 'client_id' => env('EFACTURA_CLIENT_ID'), 'client_secret' => env('EFACTURA_CLIENT_SECRET'), 'redirect_uri' => env('EFACTURA_REDIRECT_URI'), ], 'http' => [ 'timeout' => env('EFACTURA_TIMEOUT', 30), 'retry_times' => env('EFACTURA_RETRY_TIMES', 3), 'retry_delay' => env('EFACTURA_RETRY_DELAY', 5), ], 'logging' => [ 'channel' => env('EFACTURA_LOG_CHANNEL', 'efactura-sdk'), ], ];
Logging Channel (Recommended)
Add a dedicated logging channel in config/logging.php:
'efactura-sdk' => [ 'driver' => 'daily', 'path' => storage_path('logs/efactura-sdk.log'), 'level' => 'debug', 'days' => 30, ],
Rate Limiting Configuration
The SDK includes built-in rate limiting to prevent exceeding ANAF API quotas. All defaults are set to 50% of ANAF's actual limits for safety.
# Enable/disable rate limiting (default: true) EFACTURA_RATE_LIMIT_ENABLED=true # Global API calls per minute (ANAF limit: 1000, default: 500) EFACTURA_RATE_LIMIT_GLOBAL=500 # RASP file uploads per CUI per day (ANAF limit: 1000, default: 500) EFACTURA_RATE_LIMIT_RASP_UPLOAD=500 # Status queries per message per day (ANAF limit: 100, default: 50) EFACTURA_RATE_LIMIT_STATUS=50 # Simple list queries per CUI per day (ANAF limit: 1500, default: 750) EFACTURA_RATE_LIMIT_SIMPLE_LIST=750 # Paginated list queries per CUI per day (ANAF limit: 100,000, default: 50,000) EFACTURA_RATE_LIMIT_PAGINATED_LIST=50000 # Downloads per message per day (ANAF limit: 10, default: 5) EFACTURA_RATE_LIMIT_DOWNLOAD=5
ANAF Official Rate Limits:
| Endpoint | ANAF Limit | SDK Default | Scope |
|---|---|---|---|
| Global (all methods) | 1,000/minute | 500/minute | All API calls |
/upload (RASP) |
1,000/day | 500/day | Per CUI |
/stare (status) |
100/day | 50/day | Per message ID |
/lista (simple) |
1,500/day | 750/day | Per CUI |
/lista (paginated) |
100,000/day | 50,000/day | Per CUI |
/descarcare (download) |
10/day | 5/day | Per message ID |
Usage
OAuth Authentication Flow
The SDK provides a stateless OAuth implementation. You are responsible for storing tokens in your database.
Step 1: Redirect User to ANAF Authorization
use BeeCoded\EFacturaSdk\Facades\EFacturaSdkAuth; // Generate authorization URL $authUrl = EFacturaSdkAuth::getAuthorizationUrl(); // Or with custom state data $authUrl = EFacturaSdkAuth::getAuthorizationUrl(new AuthUrlSettingsData( state: ['company_id' => 123, 'user_id' => 456], scope: 'custom-scope', )); return redirect($authUrl);
Step 2: Handle OAuth Callback
use BeeCoded\EFacturaSdk\Facades\EFacturaSdkAuth; public function handleCallback(Request $request) { $code = $request->get('code'); // Exchange authorization code for tokens $tokens = EFacturaSdkAuth::exchangeCodeForToken($code); // Store tokens in YOUR database YourTokenModel::create([ 'company_id' => $companyId, 'access_token' => $tokens->accessToken, 'refresh_token' => $tokens->refreshToken, 'expires_at' => $tokens->expiresAt, ]); }
Manual Token Refresh
use BeeCoded\EFacturaSdk\Facades\EFacturaSdkAuth; $newTokens = EFacturaSdkAuth::refreshAccessToken($storedRefreshToken); // Update stored tokens $tokenModel->update([ 'access_token' => $newTokens->accessToken, 'refresh_token' => $newTokens->refreshToken, 'expires_at' => $newTokens->expiresAt, ]);
API Operations
Creating the Client
use BeeCoded\EFacturaSdk\Services\ApiClients\EFacturaClient; use BeeCoded\EFacturaSdk\Data\Auth\OAuthTokensData; // Retrieve your stored tokens $storedTokens = YourTokenModel::where('company_id', $companyId)->first(); // Create tokens DTO $tokens = new OAuthTokensData( accessToken: $storedTokens->access_token, refreshToken: $storedTokens->refresh_token, expiresAt: $storedTokens->expires_at, ); // Create client $client = EFacturaClient::fromTokens($vatNumber, $tokens);
Upload Invoice
use BeeCoded\EFacturaSdk\Data\Invoice\UploadOptionsData; use BeeCoded\EFacturaSdk\Enums\StandardType; // Basic upload $result = $client->uploadDocument($xmlContent); // With options $result = $client->uploadDocument($xmlContent, new UploadOptionsData( standard: StandardType::UBL, extern: false, // External invoice (non-Romanian supplier) selfBilled: false, // Self-billed invoice (autofactura) )); // B2C upload (to consumers) $result = $client->uploadB2CDocument($xmlContent); // Check result if ($result->isSuccessful()) { $uploadId = $result->indexIncarcare; // Store uploadId for status checking }
Check Processing Status
$status = $client->getStatusMessage($uploadId); if ($status->isReady()) { $downloadId = $status->idDescarcare; // Document is ready for download } elseif ($status->isInProgress()) { // Still processing, check again later } elseif ($status->isFailed()) { // Processing failed $errors = $status->errors; }
Download Document
$download = $client->downloadDocument($downloadId); // Save to file $download->saveTo('/path/to/invoice.zip'); // Or get content directly $zipContent = $download->content; $contentType = $download->contentType;
List Messages
use BeeCoded\EFacturaSdk\Data\Invoice\ListMessagesParamsData; use BeeCoded\EFacturaSdk\Enums\MessageFilter; // List messages from last 30 days $messages = $client->getMessages(new ListMessagesParamsData( cif: '12345678', days: 30, // 1-60 days allowed filter: MessageFilter::InvoiceSent, // Optional: T, P, E, R )); foreach ($messages->mesaje as $message) { echo $message->id; echo $message->dataCreare; echo $message->tip; }
Paginated Messages
use BeeCoded\EFacturaSdk\Data\Invoice\PaginatedMessagesParamsData; // Using timestamps (milliseconds) $messages = $client->getMessagesPaginated(new PaginatedMessagesParamsData( cif: '12345678', startTime: $startTimestampMs, endTime: $endTimestampMs, page: 1, filter: MessageFilter::InvoiceReceived, )); // Or create from Carbon dates $messages = $client->getMessagesPaginated( PaginatedMessagesParamsData::fromDateRange( cif: '12345678', startDate: now()->subDays(30), endDate: now(), page: 1, ) ); // Pagination info $messages->totalPages; $messages->totalRecords; $messages->currentPage; $messages->hasNextPage();
Validate XML
use BeeCoded\EFacturaSdk\Enums\DocumentStandardType; $validation = $client->validateXml($xmlContent, DocumentStandardType::FACT1); if ($validation->valid) { // XML is valid } else { // Validation errors $errors = $validation->errors; $details = $validation->details; }
Convert to PDF
use BeeCoded\EFacturaSdk\Enums\DocumentStandardType; // Convert without validation $pdfContent = $client->convertXmlToPdf($xmlContent, DocumentStandardType::FACT1); // Convert with validation first $pdfContent = $client->convertXmlToPdf($xmlContent, DocumentStandardType::FACT1, validate: true); file_put_contents('invoice.pdf', $pdfContent);
Verify Signature
$result = $client->verifySignature($signedXmlContent); if ($result->valid) { // Signature is valid }
Automatic Token Refresh
The SDK automatically refreshes tokens when they're about to expire (120-second buffer before expiration).
Important: ANAF uses rotating refresh tokens. When a token is refreshed, both the access token AND refresh token are replaced. The old refresh token becomes invalid.
$client = EFacturaClient::fromTokens($vatNumber, $tokens); // Make API calls $result = $client->uploadDocument($xml); $status = $client->getStatusMessage($uploadId); // IMPORTANT: Check if tokens were refreshed if ($client->wasTokenRefreshed()) { $newTokens = $client->getTokens(); // You MUST persist ALL new token values $storedTokens->update([ 'access_token' => $newTokens->accessToken, 'refresh_token' => $newTokens->refreshToken, // Critical! Old one is now invalid 'expires_at' => $newTokens->expiresAt, ]); }
Recommended Pattern:
public function uploadInvoice(string $xml, Company $company): UploadResponseData { $tokens = $this->getTokensForCompany($company); $client = EFacturaClient::fromTokens($company->vat_number, $tokens); try { $result = $client->uploadDocument($xml); return $result; } finally { // Always check for token refresh, even on errors if ($client->wasTokenRefreshed()) { $this->persistTokens($company, $client->getTokens()); } } }
Rate Limiting
The SDK automatically enforces rate limits before each API call. When a limit is exceeded, a RateLimitExceededException is thrown.
use BeeCoded\EFacturaSdk\Exceptions\RateLimitExceededException; try { $result = $client->uploadDocument($xml); } catch (RateLimitExceededException $e) { // Rate limit exceeded $remaining = $e->remaining; // 0 (no calls remaining) $retryAfter = $e->retryAfterSeconds; // Seconds until reset $message = $e->getMessage(); // Human-readable message // Wait and retry, or queue for later Log::warning("Rate limit hit: {$message}. Retry in {$retryAfter}s"); }
Checking Remaining Quota
Before making API calls, you can check remaining quota:
$rateLimiter = $client->getRateLimiter(); // Check global limit (per minute) $globalQuota = $rateLimiter->getRemainingQuota('global'); // ['limit' => 500, 'remaining' => 485, 'resetsIn' => 45] // seconds until reset // Check per-CUI limits $listQuota = $rateLimiter->getRemainingQuota('simple_list', $vatNumber); // ['limit' => 750, 'remaining' => 742, 'resetsIn' => 43200] // seconds until reset // Check per-message limits $statusQuota = $rateLimiter->getRemainingQuota('status', $uploadId); // ['limit' => 50, 'remaining' => 48, 'resetsIn' => 86400] $downloadQuota = $rateLimiter->getRemainingQuota('download', $downloadId); // ['limit' => 5, 'remaining' => 3, 'resetsIn' => 86400]
Disabling Rate Limiting
For testing or special cases, you can disable rate limiting:
EFACTURA_RATE_LIMIT_ENABLED=false
Or check status in code:
$rateLimiter = app(\BeeCoded\EFacturaSdk\Services\RateLimiter::class); if ($rateLimiter->isEnabled()) { // Rate limiting is active }
Generating Invoice XML
Using the UBL Builder
use BeeCoded\EFacturaSdk\Facades\UblBuilder; use BeeCoded\EFacturaSdk\Data\Invoice\InvoiceData; use BeeCoded\EFacturaSdk\Data\Invoice\PartyData; use BeeCoded\EFacturaSdk\Data\Invoice\AddressData; use BeeCoded\EFacturaSdk\Data\Invoice\InvoiceLineData; $invoice = new InvoiceData( invoiceNumber: 'INV-2024-001', issueDate: now(), dueDate: now()->addDays(30), currency: 'RON', paymentIban: 'RO49AAAA1B31007593840000', supplier: new PartyData( registrationName: 'Supplier Company SRL', companyId: 'RO12345678', address: new AddressData( street: 'Str. Exemplu Nr. 1', city: 'Bucuresti', postalZone: '010101', county: 'Sector 1', // Auto-sanitized to RO-B format ), registrationNumber: 'J40/1234/2020', isVatPayer: true, ), customer: new PartyData( registrationName: 'Customer Company SRL', companyId: 'RO87654321', address: new AddressData( street: 'Str. Client Nr. 2', city: 'Cluj-Napoca', postalZone: '400001', county: 'Cluj', // Auto-sanitized to RO-CJ ), isVatPayer: true, ), lines: [ new InvoiceLineData( name: 'Servicii consultanta', quantity: 10, unitPrice: 100.00, taxAmount: 190.00, // Pre-computed: 10 * 100.00 * 0.19 taxPercent: 19, unitCode: 'HUR', // Hours description: 'Consultanta IT luna ianuarie', ), new InvoiceLineData( name: 'Licenta software', quantity: 1, unitPrice: 500.00, taxAmount: 95.00, // Pre-computed: 1 * 500.00 * 0.19 taxPercent: 19, unitCode: 'C62', // Each ), ], ); // Generate UBL 2.1 XML $xml = UblBuilder::generateInvoiceXml($invoice);
Creating a Credit Note
use BeeCoded\EFacturaSdk\Enums\InvoiceTypeCode; $creditNote = new InvoiceData( invoiceNumber: 'CN-2024-001', issueDate: now(), currency: 'RON', invoiceTypeCode: InvoiceTypeCode::CreditNote, precedingInvoiceNumber: 'INV-2024-001', // BT-25: reference to the original invoice supplier: $supplier, customer: $customer, lines: [ new InvoiceLineData( name: 'Returned product', quantity: -2, // Negative = items being credited/returned unitPrice: 100.00, taxAmount: -38.00, // Negative — sign follows quantity taxPercent: 19, ), ], ); $xml = UblBuilder::generateInvoiceXml($creditNote);
Credit Note Quantity Handling (Breaking Change in v1.1)
The SDK automatically negates quantities for credit notes. ANAF treats the <CreditNote> document type as inherently negative, so line quantities must be positive in the XML. The SDK handles this sign-flip internally.
How it works: pass quantities with their business meaning, and the SDK converts them for ANAF:
| You pass | SDK sends to ANAF | Meaning |
|---|---|---|
quantity: -2 |
+2 |
Crediting 2 returned items |
quantity: 1 |
-1 |
Debiting back a discount line |
Example — credit note with a discount reversal:
$creditNote = new InvoiceData( invoiceNumber: 'CN-2024-002', issueDate: now(), currency: 'RON', invoiceTypeCode: InvoiceTypeCode::CreditNote, precedingInvoiceNumber: 'INV-2024-050', supplier: $supplier, customer: $customer, lines: [ // Crediting 3 returned items (negative → becomes positive for ANAF) new InvoiceLineData( name: 'Returned product', quantity: -3, unitPrice: 150.00, taxAmount: -85.50, // Pre-computed: -3 * 150.00 * 0.19 — sign follows quantity taxPercent: 19, ), // Reversing a discount that was on the original invoice (positive → becomes negative for ANAF) new InvoiceLineData( name: 'Discount reversal', quantity: 1, unitPrice: 50.00, taxAmount: 9.50, // Pre-computed: 1 * 50.00 * 0.19 taxPercent: 19, ), ], ); $xml = UblBuilder::generateInvoiceXml($creditNote);
Upgrading from v1.0: If your code was passing positive quantities for credit note lines and relying on them going to ANAF as-is, you must now pass negative quantities instead (the SDK will negate them to positive). If you were already passing negative quantities (as documented), no changes are needed — the SDK now correctly converts them for ANAF.
Invoice Calculations
// Line-level calculations $line = new InvoiceLineData( name: 'Product', quantity: 5, unitPrice: 100.00, taxAmount: 95.00, // Pre-computed VAT for this line taxPercent: 19, ); $line->getLineTotal(); // 500.00 (quantity * unitPrice) $line->getTaxAmount(); // 95.00 (returns the pre-computed taxAmount) $line->getLineTotalWithTax(); // 595.00 // Invoice-level calculations $invoice->getTotalExcludingVat(); // Sum of all line totals $invoice->getTotalVat(); // Sum of all per-line taxAmount values $invoice->getTotalIncludingVat(); // Total with VAT
Why taxAmount is Required (Breaking Change in v2.0)
In v1.x, the SDK calculated VAT amounts internally by grouping lines by tax rate and multiplying sum_of_base_amounts × tax_rate. This caused rounding discrepancies when your application used tax-included pricing.
The problem:
When a line item has a tax-included price (e.g., 100.00 RON including 19% VAT), your application extracts the base price by subtraction:
base = round(100.00 / 1.19, 2) = 84.03
vat = 100.00 - 84.03 = 15.97
But when the SDK grouped multiple such lines and recalculated VAT from the grouped base:
grouped_base = 84.03 + 84.03 = 168.06
grouped_vat = round(168.06 × 0.19, 2) = 31.93
Your application computed 15.97 + 15.97 = 31.94. The SDK computed 31.93. This 0.01 RON difference meant the XML total sent to ANAF didn't match your local invoice total.
The fix:
Starting in v2.0, taxAmount is a required parameter on InvoiceLineData. You pass the VAT amount you already computed for each line, and the SDK uses it directly instead of recalculating. This guarantees the XML total matches your application's total exactly.
How to compute taxAmount:
| Pricing model | Formula | Example |
|---|---|---|
| Tax-exclusive (net price) | round(quantity × unitPrice × taxPercent / 100, 2) |
qty=2, price=100, 19% → 38.00 |
| Tax-inclusive (gross price) | grossTotal - round(grossTotal / (1 + taxPercent / 100), 2) |
gross=200, 19% → 200 - 168.07 = 31.93 |
The key rule: whatever VAT amount your application stores for the line item, pass that exact value as taxAmount. The SDK will use it as-is.
taxAmount sign convention:
The taxAmount sign must follow the quantity:
- Positive quantity → positive
taxAmount - Negative quantity (credit note lines) → negative
taxAmount
The SDK's credit note sign-flip (negating quantities for ANAF) also applies to taxAmount internally — you don't need to handle this yourself.
Upgrading from v1.x: Add
taxAmountto everynew InvoiceLineData(...)call. If you were using net pricing (tax-exclusiveunitPrice), compute it asround(round(quantity * unitPrice, 2) * taxPercent / 100, 2). If you were using tax-included pricing, pass the VAT amount you already extracted from the gross total.
Address Sanitization
Romanian addresses are automatically sanitized to ISO 3166-2:RO format:
// County names are normalized 'Cluj' -> 'RO-CJ' 'Judetul Cluj' -> 'RO-CJ' 'BUCURESTI' -> 'RO-B' // Bucharest sectors are extracted 'Sector 3' -> 'RO-B' (with sector in address) 'Sectorul 1, Str. Exemplu' -> extracts sector // Diacritics are handled 'Brașov' -> 'RO-BV' 'Constanța' -> 'RO-CT'
Company Lookup
Query ANAF for company information (no authentication required):
use BeeCoded\EFacturaSdk\Facades\AnafDetails; // Single company lookup $result = AnafDetails::getCompanyData('RO12345678'); if ($result->success) { $company = $result->first(); echo $company->name; // Company name echo $company->cui; // CUI without RO prefix echo $company->getVatNumber(); // CUI with RO prefix echo $company->address; // General address echo $company->registrationNumber; // J40/1234/2020 // VAT status $company->isVatPayer; $company->vatRegistrationDate; $company->vatDeregistrationDate; // Special regimes $company->isSplitVat; // Split VAT payment $company->isRtvai; // VAT on collection // Status $company->isActive(); // Not inactive and not deregistered $company->isInactive; $company->isDeregistered; // Detailed addresses $company->headquartersAddress; // AddressData object $company->fiscalDomicileAddress; // AddressData object $company->getPrimaryAddress(); // Returns headquarters or fiscal } // Batch lookup (up to 500 companies) $result = AnafDetails::batchGetCompanyData([ 'RO12345678', 'RO87654321', '11223344', // RO prefix is optional ]); foreach ($result->companies as $company) { // Process each company } // Check for not found foreach ($result->notFound as $cui) { echo "Company not found: $cui"; } // Validate VAT code format $isValid = AnafDetails::isValidVatCode('RO12345678'); // true
Validators
VAT Number Validation
use BeeCoded\EFacturaSdk\Support\Validators\VatNumberValidator; VatNumberValidator::isValid('RO12345678'); // true VatNumberValidator::isValid('12345678'); // true (2-10 digits) VatNumberValidator::isValid('invalid'); // false VatNumberValidator::normalize('12345678'); // 'RO12345678' VatNumberValidator::stripPrefix('RO12345678'); // '12345678'
CNP Validation
use BeeCoded\EFacturaSdk\Support\Validators\CnpValidator; CnpValidator::isValid('1234567890123'); // Validates checksum CnpValidator::isValid('0000000000000'); // true (special ANAF case)
Date Helpers
use BeeCoded\EFacturaSdk\Support\DateHelper; // Format for ANAF API DateHelper::formatForAnaf(now()); // '2024-01-15' DateHelper::formatForAnaf('2024-01-15'); // '2024-01-15' // Timestamps in milliseconds (for paginated messages) DateHelper::toTimestamp(now()); // 1705312800000 // Day range for queries [$start, $end] = DateHelper::getDayRange('2024-01-15'); // $start = 1705269600000 (00:00:00.000) // $end = 1705355999999 (23:59:59.999) // Validate days parameter DateHelper::isValidDaysParameter(30); // true (1-60 allowed) DateHelper::isValidDaysParameter(100); // false
Enums
StandardType
StandardType::UBL // 'UBL' - UBL 2.1 format StandardType::CN // 'CN' - Credit Note StandardType::CII // 'CII' - Cross Industry Invoice StandardType::RASP // 'RASP' - Response
DocumentStandardType
DocumentStandardType::FACT1 // 'FACT1' - Invoice DocumentStandardType::FCN // 'FCN' - Credit Note
MessageFilter
MessageFilter::InvoiceSent // 'T' - Sent invoices MessageFilter::InvoiceReceived // 'P' - Received invoices MessageFilter::InvoiceErrors // 'E' - Errors MessageFilter::BuyerMessage // 'R' - Buyer messages
InvoiceTypeCode
Valid codes per ANAF BR-RO-020 schematron rule:
// Invoice document types (generates <Invoice> XML) InvoiceTypeCode::CommercialInvoice // '380' - Standard commercial invoice InvoiceTypeCode::CorrectedInvoice // '384' - Corrected invoice InvoiceTypeCode::SelfBilledInvoice // '389' - Self-billed invoice (autofactura) InvoiceTypeCode::AccountingInvoice // '751' - Invoice for accounting purposes // Credit note (generates <CreditNote> XML) InvoiceTypeCode::CreditNote // '381' - Credit note // Helper methods $type->isCreditNote(); // true for 381 $type->isInvoice(); // true for 380, 384, 389, 751
Note: The SDK automatically generates the correct UBL document type. Code 381 generates a <CreditNote> document with <CreditNoteTypeCode> and <CreditNoteLine> elements, while all other codes generate an <Invoice> document.
Exception Handling
use BeeCoded\EFacturaSdk\Exceptions\AuthenticationException; use BeeCoded\EFacturaSdk\Exceptions\ValidationException; use BeeCoded\EFacturaSdk\Exceptions\ApiException; use BeeCoded\EFacturaSdk\Exceptions\RateLimitExceededException; use BeeCoded\EFacturaSdk\Exceptions\XmlParsingException; try { $result = $client->uploadDocument($xml); } catch (AuthenticationException $e) { // OAuth token invalid or expired (and refresh failed) // User needs to re-authenticate } catch (RateLimitExceededException $e) { // Rate limit exceeded $retryAfter = $e->retryAfterSeconds; // Seconds until limit resets // Queue for later or wait } catch (ValidationException $e) { // Input validation failed (empty XML, invalid parameters) $message = $e->getMessage(); } catch (ApiException $e) { // API call failed $statusCode = $e->statusCode; $details = $e->details; } catch (XmlParsingException $e) { // Failed to parse XML response from ANAF }
Testing
When testing your application, you can mock the SDK services:
use BeeCoded\EFacturaSdk\Contracts\AnafAuthenticatorInterface; use BeeCoded\EFacturaSdk\Contracts\AnafDetailsClientInterface; // In your test $this->mock(AnafAuthenticatorInterface::class, function ($mock) { $mock->shouldReceive('exchangeCodeForToken') ->andReturn(new OAuthTokensData( accessToken: 'test-token', refreshToken: 'test-refresh', expiresAt: now()->addHour(), )); });
AI Assistant Integration (MCP)
This package includes an MCP server that helps AI coding assistants understand the SDK's DTOs, API methods, and conventions.
Setup: Add to your AI tool's MCP configuration:
{
"mcpServers": {
"efactura-sdk": {
"command": "node",
"args": ["vendor/bee-coded/laravel-efactura-sdk/mcp/dist/index.js"]
}
}
}
Requires Node.js 18+.
The MCP server provides these tools:
| Tool | Description |
|---|---|
get-sdk-docs |
Documentation for topics: overview, invoice-flow, credit-notes, tax-calculation, oauth-flow, error-handling, address-sanitization, rate-limiting, company-lookup |
get-dto-structure |
Complete structure of any DTO (InvoiceData, InvoiceLineData, PartyData, etc.) |
get-enum-values |
All values for any enum (InvoiceTypeCode, MessageFilter, etc.) |
get-config-reference |
Full configuration schema with env vars and defaults |
get-api-reference |
API documentation for services (EFacturaClient, AnafAuthenticator, etc.) |
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.