PHPackages                             bee-coded/laravel-efactura-sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. bee-coded/laravel-efactura-sdk

ActiveLibrary[Payment Processing](/categories/payments)

bee-coded/laravel-efactura-sdk
==============================

Laravel SDK for Romania's ANAF e-Factura electronic invoicing system

v2.1.1(2mo ago)2501Apache-2.0PHPPHP ^8.4

Since Feb 4Pushed 2mo agoCompare

[ Source](https://github.com/BEE-CODED/laravel-efactura-sdk)[ Packagist](https://packagist.org/packages/bee-coded/laravel-efactura-sdk)[ RSS](/packages/bee-coded-laravel-efactura-sdk/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (5)Dependencies (18)Versions (17)Used By (1)

Laravel e-Factura SDK
=====================

[](#laravel-e-factura-sdk)

A Laravel package for integrating with Romania's ANAF e-Factura (electronic invoicing) system.

Features
--------

[](#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
------------

[](#requirements)

- PHP 8.4+
- Laravel 11.0+
- Valid ANAF OAuth credentials

Installation
------------

[](#installation)

```
composer require bee-coded/laravel-efactura-sdk
```

Publish the configuration file:

```
php artisan vendor:publish --tag=efactura-sdk-config
```

Configuration
-------------

[](#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

[](#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)

[](#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

[](#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:**

EndpointANAF LimitSDK DefaultScopeGlobal (all methods)1,000/minute500/minuteAll API calls`/upload` (RASP)1,000/day500/dayPer CUI`/stare` (status)100/day50/dayPer message ID`/lista` (simple)1,500/day750/dayPer CUI`/lista` (paginated)100,000/day50,000/dayPer CUI`/descarcare` (download)10/day5/dayPer message IDUsage
-----

[](#usage)

### OAuth Authentication Flow

[](#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

[](#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

[](#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

[](#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

[](#api-operations)

#### Creating the Client

[](#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

[](#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

[](#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-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

[](#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

[](#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

[](#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

[](#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

[](#verify-signature)

```
$result = $client->verifySignature($signedXmlContent);

if ($result->valid) {
    // Signature is valid
}
```

### Automatic Token Refresh

[](#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

[](#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

[](#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

[](#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

[](#generating-invoice-xml)

#### Using the UBL Builder

[](#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

[](#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)

[](#credit-note-quantity-handling-breaking-change-in-v11)

**The SDK automatically negates quantities for credit notes.** ANAF treats the `` 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 passSDK sends to ANAFMeaning`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

[](#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)

[](#why-taxamount-is-required-breaking-change-in-v20)

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 modelFormulaExampleTax-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 `taxAmount` to every `new InvoiceLineData(...)` call. If you were using net pricing (tax-exclusive `unitPrice`), compute it as `round(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

[](#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

[](#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

[](#validators)

#### VAT Number Validation

[](#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

[](#cnp-validation)

```
use BeeCoded\EFacturaSdk\Support\Validators\CnpValidator;

CnpValidator::isValid('1234567890123'); // Validates checksum
CnpValidator::isValid('0000000000000'); // true (special ANAF case)
```

### Date Helpers

[](#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
-----

[](#enums)

### StandardType

[](#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)

```
DocumentStandardType::FACT1  // 'FACT1' - Invoice
DocumentStandardType::FCN    // 'FCN' - Credit Note
```

### MessageFilter

[](#messagefilter)

```
MessageFilter::InvoiceSent     // 'T' - Sent invoices
MessageFilter::InvoiceReceived // 'P' - Received invoices
MessageFilter::InvoiceErrors   // 'E' - Errors
MessageFilter::BuyerMessage    // 'R' - Buyer messages
```

### InvoiceTypeCode

[](#invoicetypecode)

Valid codes per ANAF BR-RO-020 schematron rule:

```
// Invoice document types (generates  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  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 `` document with `` and `` elements, while all other codes generate an `` document.

Exception Handling
------------------

[](#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
-------

[](#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)
------------------------------

[](#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:

ToolDescription`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
-------

[](#license)

Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.

###  Health Score

45

—

FairBetter than 93% of packages

Maintenance86

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity60

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Recently: every ~8 days

Total

15

Last Release

66d ago

Major Versions

v0.10 → v1.02026-02-10

v1.1 → v2.02026-03-13

### Community

Maintainers

![](https://www.gravatar.com/avatar/678a18cd4a1c11313dd00d0e5a4c50aaee358d8eb101a4991dd15f6c10edee12?d=identicon)[beecoded](/maintainers/beecoded)

---

Top Contributors

[![x-dragos](https://avatars.githubusercontent.com/u/13873495?v=4)](https://github.com/x-dragos "x-dragos (25 commits)")

---

Tags

laravelinvoiceublanafeFacturaRomaniaelectronic-invoicing

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/bee-coded-laravel-efactura-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/bee-coded-laravel-efactura-sdk/health.svg)](https://phpackages.com/packages/bee-coded-laravel-efactura-sdk)
```

###  Alternatives

[laraveldaily/laravel-invoices

Missing invoices for Laravel

1.5k1.3M4](/packages/laraveldaily-laravel-invoices)[horstoeko/zugferd-laravel

A library for Laravel-Framework for creating and reading european electronic invoices

3693.6k2](/packages/horstoeko-zugferd-laravel)[musahmusah/laravel-multipayment-gateways

A Laravel Package that makes implementation of multiple payment Gateways endpoints and webhooks seamless

852.2k1](/packages/musahmusah-laravel-multipayment-gateways)[omalizadeh/laravel-multi-payment

A driver-based laravel package for online payments via multiple gateways

491.1k](/packages/omalizadeh-laravel-multi-payment)[asciisd/knet

Knet package is provides an expressive, fluent interface to KNet's payment services.

141.1k](/packages/asciisd-knet)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
