PHP code example of bee-coded / laravel-efactura-sdk
1. Go to this page and download the library: Download bee-coded/laravel-efactura-sdk library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
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);
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
}
$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 = $client->downloadDocument($downloadId);
// Save to file
$download->saveTo('/path/to/invoice.zip');
// Or get content directly
$zipContent = $download->content;
$contentType = $download->contentType;
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;
}
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();
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;
}
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);
$result = $client->verifySignature($signedXmlContent);
if ($result->valid) {
// Signature is valid
}
$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,
]);
}
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());
}
}
}
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");
}
$rateLimiter = app(\BeeCoded\EFacturaSdk\Services\RateLimiter::class);
if ($rateLimiter->isEnabled()) {
// Rate limiting is active
}
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);
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);
$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);
// 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
// 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'
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 (radiat)
$company->isInactive;
$company->isDeregistered;
// Trade-registry status (stare_inregistrare)
$company->isRegistered(); // status is "INREGISTRAT"
$company->registrationStatus; // RegistrationStatus enum (Registered|Deregistered|Unknown)
$company->registrationStatusRaw; // e.g. "RADIERE din data 29.03.2024"
$company->registrationStatusDate; // Carbon date parsed from the status string
$company->registrationDate; // Fiscal registration date (data_inregistrare)
// RO e-Factura registry
$company->isRegisteredInEFactura; // enrolled in Registrul RO e-Factura
$company->eFacturaRegistrationDate;
// VAT registration history
$company->vatPeriods; // VatPeriodData[] (perioade_TVA)
// Detailed addresses
$company->headquartersAddress; // AddressData object
$company->fiscalDomicileAddress; // AddressData object
$company->getPrimaryAddress(); // Returns headquarters or fiscal
}
// Batch lookup (up to 100 companies — ANAF v9 limit)
$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
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
}
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(),
));
});