aghfatehi / laravel-zatca
Laravel ZATCA (Fatoora) package for Saudi e-invoicing Phase 1 & Phase 2 compliance. دمج الفاتورة الإلكترونية السعودية مع لارافيل - المرحلة الأولى والثانية لهيئة الزكاة والضريبة والجمارك
Requires
- php: ^8.1|^8.2|^8.3|^8.4
- ext-curl: *
- ext-dom: *
- ext-json: *
- ext-mbstring: *
- ext-openssl: *
- illuminate/console: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.0|^10.0|^11.0
Suggests
- endroid/qr-code: Required for rendering QR code as images (PNG/SVG). Install with: composer require endroid/qr-code
- simplesoftwareio/simple-qrcode: Alternative QR code library using BaconQrCode
README
Laravel ZATCA (Fatoora) Package
Saudi Arabian e-Invoicing Compliance — Phase 1 & Phase 2
المرحلة الأولى والثانية للفاتورة الإلكترونية السعودية لهيئة الزكاة والضريبة والجمارك
ZATCA integration for Laravel — QR code generation, TLV encoding, invoice signing, clearance & reporting. دمج الفاتورة الإلكترونية مع لارافيل: المرحلة الأولى (QR) والمرحلة الثانية (التوقيع والإرسال) لهيئة الزكاة والضريبة والجمارك السعودية
Table of Contents
- Overview
- Features
- Version Matrix
- Installation
- Configuration
- Integration Scenarios
- Phase 1 -- QR Code Generation
- Phase 2 -- FATOORA API Integration
- QR Code Display on PDF / View
- API Routes
- Postman Collection
- Offline Mode & Queue Sync
- Events
- Artisan Commands
- Testing
- Security & Logging
- Project Map
- Support
Overview
laravel-zatca is a production-grade Laravel package for integrating with the ZATCA (Zakat, Tax and Customs Authority) e-invoicing system — also known as Fatoora — in the Kingdom of Saudi Arabia.
The package covers both phases of the ZATCA e-invoicing mandate:
| Phase | Description | Status |
|---|---|---|
| Phase 1 | Generate and display QR code on invoices (TLV Base64 format) | Production Ready |
| Phase 2 | Full compliance: CSR, Certificate, Signing, Clearance & Reporting via FATOORA API | Production Ready |
Flexible Integration
You can use this package in any of these modes:
- Phase 1 only — Just generate QR codes for display on PDF/View (no API calls)
- Phase 2 only — Full API integration (requires pre-existing Phase 1 QR or external QR generation)
- Both phases — Full lifecycle from QR → Signing → Submission
- Offline → Online — Generate QR codes offline, sync invoices via queue when online
What is Required vs Optional
For Phase 1 (QR generation only):
| Step | Required? |
|---|---|
Install package (composer require) |
Required |
Set ZATCA_PHASE and ZATCA_VAT_* in .env |
Required |
Call Zatca::phase1()->generateQrCodeText() in your controller |
Required |
| Display QR in your Blade view | Required |
| Publish config / views | Optional |
Install endroid/qr-code for PNG output |
Optional |
| Use Model Trait for automatic QR generation | Optional |
API Routes (/zatca/onboard, etc.) |
Optional — not needed |
| Offline Mode & Queue Sync | Optional — not needed |
| Events & Logging | Optional — not needed |
For Phase 2 (API integration):
| Step | Required? |
|---|---|
| Everything from Phase 1 | Required (if using both) |
Set ZATCA_PHASE=phase_2 or =both |
Required |
| Complete onboarding (keys + CSR + certificate) | Required |
Set ZATCA_CERTIFICATE, ZATCA_PRIVATE_KEY, ZATCA_SECRET |
Required |
Call Zatca::phase2()->signInvoice() + ->submitInvoice() |
Required |
| Publish migrations for audit logging | Optional |
| Use Queue for async sync | Optional |
API Routes (/zatca/onboard) |
Optional — alternative to CLI |
| Postman Collection | Optional — testing tool |
| Events & custom listeners | Optional |
Features
- Phase 1: TLV Base64 QR code (5 tags: Seller, VAT, Date, Total, Tax)
- Phase 2: UBL 2.1 XML invoice building & XAdES signing
- Phase 2: ECDSA secp256k1 key pair generation (OpenSSL)
- Phase 2: CSR generation for ZATCA compliance certificate
- Phase 2: Compliance check (Sandbox)
- Phase 2: Clearance & Reporting (Production)
- cURL-based HTTP client (no Guzzle dependency)
- Queue support for async invoice sync with retry logic
- Offline mode -- Generate signed XML locally, sync later
- Artisan commands for onboarding & syncing
- Event-driven architecture (InvoiceCleared, InvoiceReported, InvoiceFailed)
- PSR-4 autoloading, Service Provider auto-discovery
- Logging with PII masking, non-blocking design
- No UI/frontend assumptions -- Bring your own views
- Configurable phases via single
.envvariable
External References
This package implements technical specifications for e-invoicing. Below are links to the relevant standards and portals for your own compliance verification.
| Resource | Link |
|---|---|
| ZATCA Developer Portal (Sandbox) | https://sandbox.zatca.gov.sa |
| ZATCA Production Portal | https://zatca.gov.sa |
| E-invoicing regulations (Saudi Arabia) | https://zatca.gov.sa |
This package is built by implementing publicly available technical specifications. For official compliance requirements, always refer to ZATCA's documentation and consult with legal advisors.
Version Matrix
| Component | Version |
|---|---|
| PHP | ^8.1, ^8.2, ^8.3, ^8.4 |
| Laravel | ^9.0, ^10.0, ^11.0, ^12.0, ^13.0 |
| ZATCA API | V2 (2024+) |
| UBL Standard | 2.1 |
| Signature Algorithm | ECDSA secp256k1 + SHA-256 |
| XAdES | EPES v1.3.2 |
| QR Encoding | TLV Base64 (GS1-compatible) |
| OpenSSL | Required (for key & CSR generation) |
| cURL | Required extension |
Optional QR Dependencies
The package works out of the box without any extra packages. When you call render(), it generates an SVG QR code using a built-in Blade view.
If you need PNG output or advanced QR features, install one of:
| Package | Purpose | Notes |
|---|---|---|
endroid/qr-code |
Renders QR as PNG or SVG (requires ext-gd for PNG) | composer require endroid/qr-code |
simplesoftwareio/simple-qrcode |
Alternative QR rendering (BaconQrCode wrapper) | composer require simplesoftwareio/simple-qrcode |
How it works: If one of these packages is installed, render() uses it automatically. If not, it falls back to the built-in SVG view. No configuration needed.
Installation
composer require aghfatehi/laravel-zatca
That's it for Phase 1 — QR codes work immediately. For Phase 2 you also need OpenSSL installed on your server and a ZATCA developer account.
Publish Configuration
php artisan vendor:publish --tag=zatca-config
Publish Migrations (Optional — for Phase 2 API & audit logging)
Publish and run the migrations only if you are using the optional Phase 2 API routes (onboarding, invoice clearance/reporting):
php artisan vendor:publish --tag=zatca-migrations php artisan migrate
This creates two tables:
| Table | Purpose |
|---|---|
zatca_certificates |
Stores EGS certificate and private key after ZATCA onboarding (used for invoice signing) |
zatca_invoice_logs |
Logs every invoice submission request/response with invoice_serial_number for clearance & reporting audit trail |
You do not need these migrations if you only use Phase 1 (QR code generation).
Publish Views (Optional — to customize QR fallback)
php artisan vendor:publish --tag=zatca-views
Copies qr-code.blade.php to resources/views/vendor/zatca/ so you can customize the default SVG layout.
Note: This view is only used as a fallback when
endroid/qr-codeis not installed. If you installendroid/qr-code, the view is ignored.
Verify Installation
php artisan zatca:check
Configuration
Set these in your .env file:
# --- Phase Selection --- ZATCA_PHASE=both # phase_1, phase_2, both # --- Environment --- ZATCA_ENVIRONMENT=sandbox # sandbox | production
Env Variables Reference
| Variable | Required | Description | Where to get it |
|---|---|---|---|
ZATCA_PHASE |
Yes | Which phase to enable: phase_1, phase_2, or both |
You choose |
ZATCA_ENVIRONMENT |
Yes | sandbox for testing, production for live |
You choose |
ZATCA_EGS_UUID |
Phase 2 | Unique ID for your ERP/Government System | Generated by you (any UUID v4). Used to identify your system to ZATCA. |
ZATCA_VAT_NUMBER |
Yes | Your company VAT number (15 digits in Saudi Arabia) | Your company tax registration |
ZATCA_VAT_NAME |
Yes | Your company legal name as registered with ZATCA | Your company registration |
ZATCA_CRN_NUMBER |
Phase 2 | Commercial Registration Number | Your company commercial registry |
ZATCA_INDUSTRY |
Phase 2 | Business industry (e.g., Retail, Healthcare) | Your company profile |
ZATCA_CITY |
Phase 2 | City name (e.g., Riyadh, Jeddah) | Your business address |
ZATCA_CITY_SUBDIVISION |
Phase 2 | City district or suburb | Your business address |
ZATCA_STREET |
Phase 2 | Street name | Your business address |
ZATCA_BUILDING |
Phase 2 | Building number | Your business address |
ZATCA_PLOT_ID |
Phase 2 | Plot identification number | Your business address |
ZATCA_POSTAL_ZONE |
Phase 2 | Postal/ZIP code | Your business address |
ZATCA_BRANCH_NAME |
Phase 2 | Branch name (e.g., Main Branch) | Your business structure |
ZATCA_QUEUE_CONNECTION |
Optional | Queue driver for async sync (sync, redis, database) |
Your Laravel queue config |
ZATCA_QUEUE_NAME |
Optional | Queue name for ZATCA jobs | You choose |
ZATCA_QUEUE_TRIES |
Optional | Max retry attempts on failure | You choose |
ZATCA_QUEUE_TIMEOUT |
Optional | Job timeout in seconds | You choose |
ZATCA_RETRY_DELAY_MINUTES |
Optional | Delay between retries in minutes | You choose |
ZATCA_API_MIDDLEWARE |
Optional | Middleware group for API routes (default: api) |
You choose |
ZATCA_CERTIFICATE |
Phase 2 | Base64-encoded compliance certificate from ZATCA | ZATCA Developer Portal → after running zatca:onboard with OTP. The certificate is the binarySecurityToken returned by the compliance API. |
ZATCA_PRIVATE_KEY |
Phase 2 | Base64-encoded EC private key (secp256k1) | Generated by you via zatca:onboard or Zatca::phase2()->generateKeysAndCsr(). Store securely — this is your secret key for signing invoices. |
ZATCA_SECRET |
Phase 2 | Secret string returned by ZATCA during onboarding | ZATCA Developer Portal → returned alongside the certificate when you issue a compliance certificate with OTP. |
How the onboarding flow works
1. You run: php artisan zatca:onboard --otp=123456 --save
2. Package generates EC key pair (private_key + public_key)
3. Package creates a CSR (Certificate Signing Request)
4. Package sends CSR + OTP to ZATCA API
5. ZATCA returns:
- binarySecurityToken → save as ZATCA_CERTIFICATE
- secret → save as ZATCA_SECRET
6. Your private_key → save as ZATCA_PRIVATE_KEY
The OTP is obtained from the ZATCA Developer Portal (sandbox) or ZATCA production portal.
Full config reference
See config/zatca.php for all available options with documentation.
Phase 1 -- QR Code Generation (Basic Compliance)
Phase 1 requires no API calls. It generates a TLV-encoded Base64 QR string containing:
| Tag | Field | Example |
|---|---|---|
| 1 | Seller Name | شركة التقنية |
| 2 | VAT Number | 300000000000003 |
| 3 | Date/Time (ISO 8601) | 2024-01-01T12:00:00Z |
| 4 | Invoice Total (SAR) | 115.00 |
| 5 | VAT Total (SAR) | 15.00 |
Usage
use Aghfatehi\Zatca\Facades\Zatca; // Simple QR text generation $qrText = Zatca::phase1()->generateQrCodeText( sellerName: 'شركة التقنية', vatNumber: '300000000000003', invoiceDate: '2024-01-01T12:00:00Z', totalAmount: '115.00', taxAmount: '15.00', ); // Base64-encoded TLV string ready for embedding echo $qrText;
Using with Invoice DTO
use Aghfatehi\Zatca\DTO\InvoiceDTO; $invoice = InvoiceDTO::fromArray([ 'invoice_serial_number' => 'INV-001', 'issue_date' => '2024-01-01', 'issue_time' => '12:00:00', 'line_items' => [ [ 'id' => '1', 'name' => 'Product A', 'quantity' => 2, 'tax_exclusive_price' => 100.00, 'vat_percent' => 0.15, ], ], ]); $egsUnit = [ 'vat_name' => 'شركة التقنية', 'vat_number' => '300000000000003', ]; $qrText = Zatca::phase1()->generateQrCodeFromInvoice($invoice, $egsUnit);
Phase 2 -- FATOORA API Integration (Full Compliance)
Phase 2 requires completing the ZATCA onboarding process to obtain a compliance certificate, then signing and submitting invoices.
Step 1: Onboarding (One-time setup)
Generate EC key pair, CSR, and get compliance certificate from ZATCA:
php artisan zatca:onboard --otp=123456 --solution-name=ERP --save
Or programmatically:
// Generate keys & CSR $keys = Zatca::phase2()->generateKeysAndCsr($egsUnit, 'ERP'); // Issue compliance certificate with OTP from ZATCA portal $result = Zatca::phase2()->issueComplianceCertificate($keys['csr'], $otp); if ($result->success) { // Save these securely $certificate = $result->binarySecurityToken; $secret = $result->secret; $privateKey = $keys['private_key']; // Store in .env or database \Illuminate\Support\Facades\Env::set('ZATCA_CERTIFICATE', base64_encode($certificate)); \Illuminate\Support\Facades\Env::set('ZATCA_SECRET', $secret); \Illuminate\Support\Facades\Env::set('ZATCA_PRIVATE_KEY', base64_encode($privateKey)); }
Step 2: Sign & Submit Invoice
// Build invoice data $invoice = InvoiceDTO::fromArray([ 'invoice_serial_number' => 'EGS1-886431145-1', 'invoice_counter_number' => 2, 'issue_date' => '2024-01-01', 'issue_time' => '14:40:40', 'previous_invoice_hash' => '', 'line_items' => [ [ 'id' => '1', 'name' => 'Product A', 'quantity' => 10, 'tax_exclusive_price' => 100.00, 'vat_percent' => 0.15, ], ], ]); $egsUnit = [ 'uuid' => '6f4d20e0-6bfe-4a80-9389-7dabe6620f12', 'custom_id' => 'EGS1-886431145', 'model' => 'Desktop', 'vat_number' => '300000000000003', 'vat_name' => 'شركة التقنية', 'crn_number' => '454634645645654', 'location' => [ 'city' => 'Riyadh', 'city_subdivision' => 'West', 'street' => 'King Fahd Road', 'building' => '1234', 'plot_identification' => '0000', 'postal_zone' => '11564', ], 'branch_name' => 'Main Branch', 'branch_industry' => 'Retail', ]; // 1. Sign invoice (generates XML + hash + QR) $signed = Zatca::phase2()->signInvoice( invoice: $invoice, egsUnit: $egsUnit, certificate: $certificate, privateKey: $privateKey, ); // 2. Submit to ZATCA (auto-detects sandbox vs production) $result = Zatca::phase2()->submitInvoice( signedInvoiceXml: $signed['signed_xml'], invoiceHash: $signed['invoice_hash'], certificate: $certificate, secret: $secret, ); if ($result->success) { echo 'Invoice submitted successfully! Request ID: ' . $result->requestID; }
Using Queue for Async Sync
use Aghfatehi\Zatca\Jobs\SyncInvoiceToZatcaJob; SyncInvoiceToZatcaJob::dispatch( invoiceData: $invoice->toArray(), egsUnit: $egsUnit, certificate: $certificate, privateKey: $privateKey, secret: $secret, );
QR Code Display on PDF / View
Method 1: Blade View (Direct Rendering)
1. In the Controller — generate QR TLV:
<?php namespace App\Http\Controllers; use Aghfatehi\Zatca\Facades\Zatca; class InvoiceController extends Controller { public function show(Invoice $invoice) { $qrTlv = Zatca::phase1()->generateQrCodeFromInvoice( invoice: $invoice->toInvoiceDto(), egsUnit: [ 'vat_name' => config('zatca.egs.vat_name'), 'vat_number' => config('zatca.egs.vat_number'), ], ); return view('invoice.show', compact('invoice', 'qrTlv')); } }
2. In the Blade file — render the QR:
{{-- resources/views/invoice/show.blade.php --}} @extends('layouts.app') @section('content') <div class="invoice"> <h1>invoice No: {{ $invoice->number }}</h1> <table> @foreach ($invoice->items as $item) <tr> <td>{{ $item->name }}</td> <td>{{ $item->price }}</td> </tr> @endforeach </table> {{-- QR Code output — SVG (no deps) or PNG (with endroid/qr-code) --}} <div class="qr-section" style="text-align: center; margin-top: 20px;"> <img src="data:image/png;base64,{{ base64_encode(Zatca::qr()->render($qrTlv, 200)) }}" alt="ZATCA QR Code" style="width: 200px; height: 200px;"> </div> </div> @endsection
How it works: render() returns:
- SVG — if
endroid/qr-codeis NOT installed (default, no extra deps) - PNG binary — if
endroid/qr-codeIS installed
Both work with <img src="data:image/...;base64,...">.
Method 2: Using the Model Trait
Add the trait to your invoice model:
use Aghfatehi\Zatca\Traits\HasZatcaQrCode; class Invoice extends Model { use HasZatcaQrCode; // Customize field names (optional) protected $zatcaSellerField = 'company_name'; protected $zatcaVatField = 'vat_number'; protected $zatcaDateField = 'invoice_date'; protected $zatcaTotalField = 'total_amount'; protected $zatcaTaxField = 'tax_amount'; }
Then in your view:
{{-- Automatically generates QR from model fields --}} {!! $invoice->getZatcaQrCode(200) !!}
Method 3: PDF Generation with barryvdh/laravel-dompdf
use Barryvdh\DomPDF\Facade\Pdf; $qrText = Zatca::phase1()->generateQrCodeText( sellerName: $invoice->company_name, vatNumber: $invoice->vat_number, invoiceDate: $invoice->invoice_date->format('Y-m-d\TH:i:s\Z'), totalAmount: (string)$invoice->total_amount, taxAmount: (string)$invoice->tax_amount, ); // Generate QR as base64 image $qrBase64 = base64_encode(Zatca::qr()->render($qrText, 150)); $pdf = Pdf::loadView('invoice.pdf', compact('invoice', 'qrBase64')); return $pdf->download('invoice.pdf');
In invoice/pdf.blade.php:
<html> <head> <style> .qr-code { position: fixed; bottom: 20px; right: 20px; width: 150px; } </style> </head> <body> <h1>{{ $invoice->invoice_serial_number }}</h1> <table> <tr><th>Item</th><th>Price</th><th>VAT</th></tr> @foreach($invoice->items as $item) <tr><td>{{ $item->name }}</td><td>{{ $item->price }}</td><td>{{ $item->vat }}</td></tr> @endforeach </table> <div class="qr-code"> <img src="data:image/png;base64,{{ $qrBase64 }}" alt="ZATCA QR"> </div> </body> </html>
Method 4: PDF with mpdf (for ERP System or E-commerce)
use Mpdf\Mpdf; $mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4']); $qrText = Zatca::phase1()->generateQrCodeText(...); $qrBase64 = base64_encode(Zatca::qr()->render($qrText, 150)); $html = '<div style="position: absolute; bottom: 10mm; right: 10mm;"> <img src="@' . $qrBase64 . '" width="150" height="150"/> </div>'; $mpdf->WriteHTML($html); $mpdf->Output('invoice.pdf', 'D');
Method 5: Advanced Output (Base64, Data URI, File)
// Base64-encoded image (SVG or PNG depending on installed packages) $base64 = Zatca::qr()->renderAsBase64($qrText, 200); // Data URI ready for <img> tag $dataUri = Zatca::qr()->renderAsDataUri($qrText, 200); // Save directly to file (SVG or PNG) Zatca::qr()->renderToFile($qrText, storage_path('app/public/qr/invoice.svg'), 200);
These methods work automatically whether or not endroid/qr-code is installed.
API Routes (Optional)
The package registers HTTP API endpoints. You do not need these for basic Phase 1 or Phase 2 usage — they are an alternative to calling the package methods directly from PHP code or Artisan commands.
These routes are separate from the QR rendering methods above. By default they use the api middleware group — change it with ZATCA_API_MIDDLEWARE in your .env.
| Method | Path | Description |
|---|---|---|
POST |
/zatca/onboard |
Onboard via API — requires otp and optional solution_name |
POST |
/zatca/invoice/sync |
Dispatch a sync job — requires invoice_serial_number |
GET |
/zatca/status |
Returns current phase, environment, and enabled status |
These routes are completely independent from the QR rendering methods above. You can use them with any HTTP client (Postman, cURL, your frontend app, etc.).
Testing with cURL
Test the routes directly with cURL:
# Check package status curl -X GET http://localhost:8000/zatca/status \ -H "Accept: application/json" # Onboard with OTP curl -X POST http://localhost:8000/zatca/onboard \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"otp": "123456", "solution_name": "ERP"}' # Dispatch invoice sync curl -X POST http://localhost:8000/zatca/invoice/sync \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"invoice_serial_number": "INV-001"}'
Postman Collection (ZATCA Official)
The official ZATCA Fatoora API Postman collection covers all ZATCA endpoints (onboarding, compliance, clearance, reporting):
- Go to ZATCA Developer Portal
- Log in with your developer account
- Navigate to API Documentation → Postman Collection
- Download and import into Postman
- Set environment variables (base URL, OTP, certificates, etc.)
This collection is maintained by ZATCA and contains all the endpoints that this package calls internally.
For the package's own optional routes (/zatca/onboard, /zatca/invoice/sync, /zatca/status), use the cURL examples above.
Offline Mode & Queue Sync (Optional)
The package natively supports offline invoice preparation with queue-based synchronization. You do not need this for basic Phase 1 or Phase 2 — it is only useful if you want to generate and sign invoices when offline, then sync them to ZATCA later.
Offline Flow
┌─────────────────────────────────────────────────────────┐
│ OFFLINE MODE │
│ │
│ ERP System ──► Generate QR (Phase 1) │
│ ──► Sign Invoice (Phase 2) │
│ ──► Save Signed XML Locally │
│ ──► Dispatch SyncInvoiceToZatcaJob │
│ │
│ When online ──► Queue Worker picks up job │
│ ──► Submits to ZATCA API │
│ ──► Fires InvoiceCleared/InvoiceReported │
│ ──► Logs result │
└─────────────────────────────────────────────────────────┘
Configuration
# Use database queue for persistence across restarts ZATCA_QUEUE_CONNECTION=database ZATCA_QUEUE_NAME=zatca # Retry settings ZATCA_QUEUE_TRIES=5 ZATCA_RETRY_DELAY_MINUTES=60
Run the Queue Worker
php artisan queue:work --queue=zatca --tries=3 --delay=3600
Sync Pending Invoices Manually
# Sync a specific invoice php artisan zatca:sync --invoice=INV-001 # Sync all pending invoices php artisan zatca:sync --all
Data Flow
Phase 1 Flow:
Invoice Data (5 tags) --> TLV Encoder --> Base64 --> QR Image
Phase 2 Flow:
1. Generate EC Key Pair (secp256k1)
2. Generate CSR
3. Submit CSR + OTP --> ZATCA API --> Compliance Certificate
4. Build UBL 2.1 XML Invoice
5. Hash Invoice (SHA-256)
6. Create Digital Signature (ECDSA)
7. Generate TLV QR (9 tags)
8. Embed XAdES Signature
9. Submit to ZATCA:
- Sandbox: POST /compliance/invoices
- Production: POST /invoices/clearance OR /reporting
10. Handle Response --> Fire Events --> Log
Events (Optional)
The package fires events that you can listen to in your application. Not needed for basic usage — only useful if you want to react to invoice submission results (e.g., update your database when an invoice is cleared).
| Event | Description | Payload |
|---|---|---|
InvoiceCleared |
Invoice successfully cleared (production) | invoiceData, ComplianceResultDTO |
InvoiceReported |
Invoice reported successfully (sandbox) | invoiceData, ComplianceResultDTO |
InvoiceComplianceChecked |
Compliance check completed | invoiceData, ComplianceResultDTO |
InvoiceFailed |
Invoice submission failed | invoiceData, errorMessage |
Example Listener
namespace App\Listeners; use Aghfatehi\Zatca\Events\InvoiceCleared; class UpdateInvoiceStatus { public function handle(InvoiceCleared $event): void { $serial = $event->invoiceData['invoice_serial_number']; // Update your invoice status in DB Invoice::where('serial_number', $serial) ->update(['zatca_status' => 'cleared']); } }
Register in EventServiceProvider:
protected $listen = [ \Aghfatehi\Zatca\Events\InvoiceCleared::class => [ \App\Listeners\UpdateInvoiceStatus::class, ], \Aghfatehi\Zatca\Events\InvoiceFailed::class => [ \App\Listeners\MarkInvoiceAsFailed::class, ], ];
Artisan Commands
| Command | Required | Description |
|---|---|---|
php artisan zatca:onboard |
Phase 2 | Interactive onboarding wizard (generates keys, CSR, gets certificate) |
php artisan zatca:check |
Phase 1 & 2 | Check package readiness (OpenSSL, config, etc.). Run after install. |
php artisan zatca:sync |
Optional | Sync invoices to ZATCA via queue (single or all pending) |
Testing
composer test
Or with PHPUnit directly:
vendor/bin/phpunit
Security & Logging (Optional)
Logging is enabled by default but not required for basic Phase 1 or Phase 2. It is useful for audit trails and debugging.
Logging Design
- Levels:
info,warning,erroronly - Non-blocking: Uses Laravel's async-safe log channel
- PII Masking: Automatically masks
otp,secret,password,private_key,csr - Truncation: Values exceeding 500 chars are truncated
- Audit trail: Optional
zatca_invoice_logsdatabase table for compliance tracking
Security Best Practices
- Never store private keys or secrets in code
- Use
.envvariables or a secrets manager for credentials - Restrict queue worker access to authorized personnel
- Enable PII masking in production (
ZATCA_LOG_MASK_PII=true) - Use HTTPS for all ZATCA API communication (enforced by cURL)
- Rotate OTP and secrets according to ZATCA guidelines
Integration Scenarios
Scenario 1: New Project — Both Phases
ZATCA_PHASE=both ZATCA_ENVIRONMENT=sandbox
Start with Phase 1 QR codes immediately, then add Phase 2 when ready.
Scenario 2: Phase 1 Only (Existing System)
ZATCA_PHASE=phase_1
Just generate QR codes. No API keys needed.
Scenario 3: Phase 2 Only (Already have Phase 1)
ZATCA_PHASE=phase_2
You already generate QR codes elsewhere (or via another package). This package handles the API integration.
Scenario 4: External API Integration
If your ERP exposes an API, you can use the package's internal services directly:
use Aghfatehi\Zatca\Services\Phase2Service; class YourController { public function sync(Request $request, Phase2Service $phase2) { $invoice = InvoiceDTO::fromArray($request->all()); $result = $phase2->signInvoice($invoice, ...); // ... } }
Support
- Issues: github.com/aghfatehi/laravel-zatca/issues
- Source: github.com/aghfatehi/laravel-zatca
- ZATCA Portal: sandbox.zatca.gov.sa