bee-coded/laravel-efactura

Laravel wrapper for e-Factura SDK with token storage, job scheduling, and easy model integration

Maintainers

Package info

github.com/BEE-CODED/laravel-efactura

pkg:composer/bee-coded/laravel-efactura

Statistics

Installs: 50

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v2.1.1 2026-03-13 20:16 UTC

This package is auto-updated.

Last update: 2026-03-13 20:17:37 UTC


README

A Laravel package that wraps bee-coded/laravel-efactura-sdk to provide token storage, job scheduling, and easy model integration for Romanian e-Factura (ANAF SPV) compliance.

Features

  • Token Management - OAuth token storage per CUI with automatic refresh
  • Background Jobs - Ready-to-use jobs for invoice uploads, status checks, and message syncing
  • Model Integration - Simple interface + trait pattern for your invoice models
  • Event-Driven - Events for all key operations (uploads, failures, received invoices)
  • Minimal Setup - Auto-discovery, publishable config and migrations

Requirements

  • PHP 8.4+
  • Laravel 11.x or 12.x
  • ANAF SPV OAuth credentials

Installation

composer require bee-coded/laravel-efactura

Publish the configuration and migrations:

php artisan vendor:publish --tag=efactura-config
php artisan vendor:publish --tag=efactura-migrations
php artisan migrate

Configuration

Environment Variables

# SDK Configuration (required)
EFACTURA_SANDBOX=true
EFACTURA_CLIENT_ID=your-anaf-client-id
EFACTURA_CLIENT_SECRET=your-anaf-client-secret
EFACTURA_REDIRECT_URI=https://your-app.com/efactura/callback

# Package Configuration
EFACTURA_ENABLED=true
EFACTURA_UPLOAD_ENABLED=true
EFACTURA_DOWNLOAD_RECEIVED=false
EFACTURA_SYNC_MESSAGES=true

# Storage
EFACTURA_STORAGE_DISK=local
EFACTURA_STORAGE_PATH=efactura

# Queue (null = default queue)
EFACTURA_QUEUE=null

# Rate Limit Handling
EFACTURA_RATE_LIMIT_RETRY_HOURS=24
EFACTURA_RATE_LIMIT_RETRY_BATCH=250
EFACTURA_RATE_LIMIT_RETRY_MAX_DAYS=7

# Routes
EFACTURA_ROUTES_ENABLED=true
EFACTURA_ROUTES_PREFIX=efactura
EFACTURA_SUCCESS_REDIRECT=/
EFACTURA_ERROR_REDIRECT=/

Config File

The configuration file (config/efactura.php) allows you to:

  • Enable/disable the entire package or specific features
  • Configure file storage for XML and ZIP files
  • Set job schedules using cron expressions
  • Customize OAuth callback routes

Model Integration

1. Implement the Interface

Your invoice model must implement EFacturaUploadableInterface:

<?php

namespace App\Models;

use BeeCoded\EFactura\Contracts\EFacturaUploadableInterface;
use BeeCoded\EFactura\Traits\HasEfacturaUpload;
use BeeCoded\EFacturaSdk\Data\Invoice\InvoiceData;
use BeeCoded\EFacturaSdk\Data\Invoice\PartyData;
use BeeCoded\EFacturaSdk\Data\Invoice\AddressData;
use BeeCoded\EFacturaSdk\Data\Invoice\InvoiceLineData;
use Illuminate\Database\Eloquent\Model;

class Invoice extends Model implements EFacturaUploadableInterface
{
    use HasEfacturaUpload;

    /**
     * Transform this model into SDK's InvoiceData DTO.
     */
    public function toEfacturaData(): InvoiceData
    {
        return new InvoiceData(
            invoiceNumber: $this->number,
            issueDate: $this->issued_at,
            dueDate: $this->due_at,
            currency: $this->currency,
            supplier: new PartyData(
                registrationName: $this->company->name,
                companyId: $this->company->vat_number,
                address: new AddressData(
                    street: $this->company->address,
                    city: $this->company->city,
                    postalZone: $this->company->postal_code,
                    countryCode: $this->company->country_code,
                ),
                isVatPayer: $this->company->is_vat_payer,
            ),
            customer: new PartyData(
                registrationName: $this->customer->name,
                companyId: $this->customer->vat_number,
                address: new AddressData(
                    street: $this->customer->address,
                    city: $this->customer->city,
                    postalZone: $this->customer->postal_code,
                    countryCode: $this->customer->country_code,
                ),
                isVatPayer: $this->customer->is_vat_payer,
            ),
            lines: $this->lines->map(fn ($line) => new InvoiceLineData(
                name: $line->description,
                quantity: $line->quantity,
                unitPrice: $line->unit_price,
                taxAmount: $line->vat_amount,  // Pre-computed VAT for this line
                taxPercent: $line->vat_rate,
            ))->all(),
            paymentIban: $this->company->iban,
        );
    }

    /**
     * Get the CUI for this invoice (determines which token to use).
     */
    public function getEfacturaCui(): string
    {
        return $this->company->cui; // Without RO prefix
    }
}

2. Available Trait Methods

The HasEfacturaUpload trait provides:

// Relationship
$invoice->efacturaUpload; // The EfacturaUpload model

// Status checks
$invoice->isUploadedToEfactura();  // bool
$invoice->getEfacturaStatus();     // ?UploadStatus enum
$invoice->isEfacturaProcessed();   // bool (completed or failed)

// File paths
$invoice->getEfacturaXmlPath();      // ?string
$invoice->getEfacturaResponsePath(); // ?string
$invoice->getEfacturaErrors();       // ?array

// Query scopes
Invoice::notUploadedToEfactura()->get();           // Not yet queued
Invoice::efacturaPending()->get();                  // Queued, awaiting upload
Invoice::efacturaInProgress()->get();               // Currently uploading/processing
Invoice::efacturaCompleted()->get();                // Successfully processed
Invoice::efacturaFailed()->get();                   // Failed
Invoice::efacturaProcessed()->get();                // Terminal state (completed or failed)
Invoice::efacturaAwaitingResponse()->get();         // Completed but response not downloaded
Invoice::withEfacturaStatus(UploadStatus::Pending)->get(); // Specific status

Usage

Queue an Invoice for Upload

use BeeCoded\EFactura\Facades\EFactura;

// Standard B2B upload
$upload = EFactura::queueUpload($invoice);

// With options
$upload = EFactura::queueUpload($invoice, [
    'standard' => 'UBL',      // UBL, CN, CII, RASP
    'extern' => false,        // External/non-Romanian supplier
    'self_billed' => false,   // Self-billed/autofactura
]);

// B2C upload
$upload = EFactura::queueB2CUpload($invoice);

Queue a Credit Note for Upload

Credit notes work the same way as invoices — just set invoiceTypeCode to CreditNote and reference the original invoice:

use BeeCoded\EFacturaSdk\Data\Invoice\InvoiceData;
use BeeCoded\EFacturaSdk\Data\Invoice\InvoiceLineData;
use BeeCoded\EFacturaSdk\Enums\InvoiceTypeCode;

$creditNote = new InvoiceData(
    invoiceNumber: 'CN-2024-001',
    issueDate: now(),
    currency: 'RON',
    invoiceTypeCode: InvoiceTypeCode::CreditNote,
    precedingInvoiceNumber: 'INV-2024-001',
    supplier: $supplier,
    customer: $customer,
    lines: [
        new InvoiceLineData(
            name: 'Returned product',
            quantity: -3,        // negative = items being credited
            unitPrice: 150.00,
            taxAmount: -85.50,   // sign follows quantity: -3 * 150.00 * 0.19
            taxPercent: 19,
        ),
    ],
);

Note: The SDK (v1.1+) automatically negates credit note line quantities before sending to ANAF (which expects positive values in <CreditNote> documents). Pass negative quantities for items being credited and positive for debit-back lines (e.g., discount reversals).

$upload = EFactura::queueUpload($creditNoteModel);

The taxAmount Parameter (Required since SDK v2.0)

Every InvoiceLineData requires a taxAmount — the pre-computed VAT amount for that line. The SDK uses this value directly in the XML instead of recalculating VAT internally.

Why this matters: In v1.x, the SDK grouped lines by tax rate and recalculated VAT as sum_of_bases × rate. This caused rounding discrepancies (typically 0.01 RON) when your application used tax-included pricing, because extracting VAT by subtraction (gross - net) can produce different results than multiplying (net × rate) after rounding. By passing your pre-computed taxAmount, the XML total matches your application's total exactly.

How to compute it:

// Tax-exclusive pricing (you store the net unit price):
$taxAmount = round(round($quantity * $unitPrice, 2) * $vatRate / 100, 2);

// Tax-inclusive pricing (you store the gross price and extract the net):
$basePrice = round($grossPrice / (1 + $vatRate / 100), 2);
$taxAmount = $grossPrice - $basePrice; // This is what you should pass

Sign convention: The taxAmount sign follows the quantity — negative for credit note lines (negative qty), positive for regular lines.

Upgrading from SDK v1.x: Add taxAmount to every InvoiceLineData in your toEfacturaData() method. Pass the VAT amount your application already computes for each line item.

Process Upload Immediately

// Queue and process immediately
$upload = EFactura::queueUpload($invoice);
EFactura::processUpload($upload);

Access the SDK Client

For advanced operations, get an authenticated SDK client:

use BeeCoded\EFacturaSdk\Enums\DocumentStandardType;

$client = EFactura::client('12345678'); // CUI

// Validate XML
$result = $client->validateXml($xml, DocumentStandardType::FACT1);

// Convert to PDF
$pdf = $client->convertXmlToPdf($xml, DocumentStandardType::FACT1);

// Get messages
$messages = $client->getMessages($params);

Company Lookup (No Auth Required)

Use the SDK directly for company lookups:

use BeeCoded\EFacturaSdk\Facades\AnafDetails;

$company = AnafDetails::getCompanyData('12345678');
$companies = AnafDetails::batchGetCompanyData(['12345678', '87654321']);

OAuth Flow

1. Redirect to ANAF

The package provides routes for OAuth:

GET /efactura/auth/{cui}  → Redirects to ANAF OAuth
GET /efactura/callback    → Handles OAuth callback

In your application:

// In a controller or Livewire component
return redirect()->route('efactura.auth', ['cui' => '12345678']);

Or generate the URL manually:

$url = EFactura::getAuthorizationUrl('12345678');

2. Handle the Callback

The package automatically:

  • Validates the OAuth state (CSRF protection)
  • Exchanges the code for tokens
  • Stores the tokens in the database
  • Fires the TokenStored event
  • Redirects to your configured success/error URL

3. Listen to Events

// In EventServiceProvider or a listener
use BeeCoded\EFactura\Events\TokenStored;

Event::listen(TokenStored::class, function (TokenStored $event) {
    $token = $event->token;

    // Notify user, log, etc.
    Log::info("e-Factura authorized for CUI: {$token->cui}");
});

Job Scheduling (Required)

Important: This package provides jobs but does NOT schedule them automatically. You must register the job schedules in your application.

Register Jobs in Your Scheduler

Add the following to your bootstrap/app.php:

use BeeCoded\EFactura\Jobs\ProcessPendingUploads;
use BeeCoded\EFactura\Jobs\CheckUploadStatuses;
use BeeCoded\EFactura\Jobs\DownloadResponses;
use BeeCoded\EFactura\Jobs\DownloadReceivedInvoices;
use BeeCoded\EFactura\Jobs\SyncMessages;
use BeeCoded\EFactura\Jobs\RetryRateLimitedUploads;

->withSchedule(function (Schedule $schedule): void {
    // Upload pending invoices to ANAF
    $schedule->job(new ProcessPendingUploads)->everyFiveMinutes();

    // Check processing status at ANAF
    $schedule->job(new CheckUploadStatuses)->everyTenMinutes();

    // Download response ZIPs for completed uploads
    $schedule->job(new DownloadResponses)->everyFifteenMinutes();

    // Retry uploads that failed due to rate limiting
    $schedule->job(new RetryRateLimitedUploads)->everyTenMinutes();

    // Download received invoices (if feature enabled)
    $schedule->job(new DownloadReceivedInvoices)->everyFourHours();

    // Sync message list from ANAF
    $schedule->job(new SyncMessages)->hourly();
})

Adjust the schedules to fit your application's needs. All jobs accept an optional $cui parameter to process only a specific CUI:

Schedule::job(new ProcessPendingUploads('12345678'))->everyFiveMinutes();

Dispatch a Single Upload or Status Check

For immediate processing of a single upload, dispatch the single-model jobs:

use BeeCoded\EFactura\Jobs\ProcessSingleUpload;
use BeeCoded\EFactura\Jobs\CheckSingleUploadStatus;

// Queue and dispatch a single upload immediately
$upload = EFactura::queueUpload($invoice);
ProcessSingleUpload::dispatch($upload);

// Check status for a specific upload
CheckSingleUploadStatus::dispatch($upload);

Available Jobs

Batch Jobs (Scheduled)

Job Purpose Suggested Schedule
ProcessPendingUploads Upload all pending invoices to ANAF Every 5 minutes
CheckUploadStatuses Check processing status at ANAF Every 10 minutes
DownloadResponses Download response ZIPs Every 15 minutes
DownloadReceivedInvoices Download received invoices Every 4 hours
SyncMessages Sync message list from ANAF Every hour
RetryRateLimitedUploads Reset rate-limited failures back to pending Every 10 minutes

Single-Model Jobs (On-Demand)

Job Purpose
ProcessSingleUpload Process a single upload immediately
CheckSingleUploadStatus Check status for a single upload

Job Configuration

Standard jobs (batch processing, status checks, downloads):

  • Tries: 3
  • Timeout: 120 seconds
  • Backoff: 60s, 180s, 300s (progressive)

Upload jobs (ProcessSingleUpload) have rate-limit-aware retry:

  • Timeout: 120 seconds
  • Max Exceptions: 3 (actual errors only — rate-limit releases don't count)
  • Retry Window: 24 hours (configurable via EFACTURA_RATE_LIMIT_RETRY_HOURS)
  • When the SDK's global rate limit quota is exhausted, the job releases itself back to the queue with a delay matching the quota reset time, instead of failing
  • If a race condition causes a rate-limit failure during upload, the job resets the upload to pending and releases with a 60-second delay

Queue Configuration

Jobs are dispatched to the queue specified in config/efactura.php:

'queue' => env('EFACTURA_QUEUE', null), // null = default queue

Set EFACTURA_QUEUE=efactura in your .env to use a dedicated queue. This allows you to run a separate worker for e-Factura jobs:

php artisan queue:work --queue=efactura

Rate Limit Configuration

The SDK provides client-side rate limiting to stay within ANAF quotas. Upload jobs are rate-limit-aware and will delay instead of failing when quotas are exhausted. For uploads that do fail due to rate limiting (e.g., race conditions), schedule RetryRateLimitedUploads to automatically reset them.

# How long upload jobs can keep retrying (hours, default: 24)
EFACTURA_RATE_LIMIT_RETRY_HOURS=24

# Max failed uploads to reset per retry run (default: 250)
EFACTURA_RATE_LIMIT_RETRY_BATCH=250

# Don't retry uploads older than this many days (default: 7)
EFACTURA_RATE_LIMIT_RETRY_MAX_DAYS=7

Queue Worker

Ensure your queue worker is running:

php artisan queue:work

Artisan Commands

# Display OAuth URL for a CUI
php artisan efactura:auth 12345678

# Process pending uploads
php artisan efactura:upload
php artisan efactura:upload --cui=12345678

# Check statuses and download responses
php artisan efactura:status
php artisan efactura:status --cui=12345678

# Sync messages from ANAF
php artisan efactura:sync
php artisan efactura:sync --cui=12345678

Events

Listen to these events for custom logic:

Event Fired When Payload
TokenStored OAuth callback stores new token EfacturaToken $token
TokenRefreshed Token auto-refreshed EfacturaToken $token
InvoiceUploaded Invoice successfully uploaded EfacturaUpload $upload
InvoiceProcessed Response downloaded (success) EfacturaUpload $upload
InvoiceFailed Upload or processing failed EfacturaUpload $upload, array $errors
InvoiceReceived Received invoice downloaded EfacturaMessage $message

Example Listener

<?php

namespace App\Listeners;

use BeeCoded\EFactura\Events\InvoiceFailed;
use App\Notifications\EfacturaFailedNotification;

class HandleFailedInvoice
{
    public function handle(InvoiceFailed $event): void
    {
        $upload = $event->upload;
        $errors = $event->errors;

        // Get the original invoice
        $invoice = $upload->uploadable;

        // Notify someone
        $invoice->company->owner->notify(
            new EfacturaFailedNotification($invoice, $errors)
        );
    }
}

Database Schema

The package creates three tables:

efactura_tokens

Stores OAuth tokens per CUI.

efactura_uploads

Tracks upload status for your invoices (polymorphic relationship).

efactura_messages

Stores synced messages from ANAF (sent, received, errors).

Upload Status Flow

Pending → Uploading → Processing → Completed
                   ↘            ↘
                    → Failed ←────┘
  • Pending: Queued, waiting to be uploaded
  • Uploading: Currently being uploaded to ANAF
  • Processing: Uploaded, waiting for ANAF to process
  • Completed: Successfully processed, response available
  • Failed: Upload or processing failed

Logging

The SDK logs all API calls to a dedicated logging channel. Add the following channel to your config/logging.php:

'channels' => [
    // ... other channels

    'efactura-sdk' => [
        'driver' => 'daily',
        'path' => storage_path('logs/efactura-sdk.log'),
        'level' => 'debug',
        'days' => 30,
    ],
],

You can customize the channel name via the EFACTURA_LOG_CHANNEL environment variable (defaults to efactura-sdk).

Testing

For testing, use the SDK's sandbox mode:

EFACTURA_SANDBOX=true

Troubleshooting

Token Not Found

Ensure you've completed the OAuth flow for the CUI:

php artisan efactura:auth 12345678

Jobs Not Running

  1. Verify the scheduler is registered
  2. Check config('efactura.enabled') is true
  3. Check feature flags are enabled
  4. Review Laravel queue worker logs

Upload Failures

Check the errors column in efactura_uploads table or listen to the InvoiceFailed event.

AI Assistant Integration (MCP)

This package and its SDK dependency both include MCP servers that help AI coding assistants understand the full e-Factura integration.

Setup: Add both to your AI tool's MCP configuration:

{
  "mcpServers": {
    "efactura-sdk": {
      "command": "node",
      "args": ["vendor/bee-coded/laravel-efactura-sdk/mcp/dist/index.js"]
    },
    "efactura": {
      "command": "node",
      "args": ["vendor/bee-coded/laravel-efactura/mcp/dist/index.js"]
    }
  }
}

Requires Node.js 18+.

The wrapper MCP server provides these tools:

Tool Description
get-wrapper-docs Documentation topics: overview, setup, upload-pipeline, token-management, commands
get-model-integration-guide Complete guide for integrating a model with e-Factura
get-event-reference All events: TokenStored, TokenRefreshed, InvoiceUploaded, InvoiceProcessed, InvoiceFailed, InvoiceReceived
get-job-reference All 8 background jobs with scheduling guidance
get-wrapper-config-reference Full configuration schema with env vars and defaults

The SDK MCP server (efactura-sdk) provides DTOs, enums, API reference, and SDK-level documentation.

License

This package is open-sourced software licensed under the Apache 2.0 License.

Credits