corecave/laravel-zatca

Laravel package for ZATCA e-invoicing integration (Saudi Arabia Phase 2)

Installs: 91

Dependents: 0

Suggesters: 0

Security: 0

Stars: 6

Watchers: 0

Forks: 1

Open Issues: 0

pkg:composer/corecave/laravel-zatca

v1.2.0 2025-12-16 15:51 UTC

This package is auto-updated.

Last update: 2026-01-16 16:40:45 UTC


README

A comprehensive Laravel package for integrating with Saudi Arabia's ZATCA (Zakat, Tax and Customs Authority) e-invoicing system (FATOORA platform).

Features

  • CSR generation and certificate management
  • Invoice XML generation (UBL 2.1 compliant)
  • Digital signing with ECDSA (secp256k1)
  • QR code generation (TLV format with 9 tags)
  • QR code image generation (PNG/SVG) for invoices
  • Invoice reporting (B2C simplified invoices)
  • Invoice clearance (B2B standard invoices)
  • Credit and debit note handling
  • Hash chain management (ICV & PIH)
  • Sandbox, simulation, and production environments

Requirements

  • PHP 8.1+
  • Laravel 10.x, 11.x, or 12.x
  • OpenSSL extension
  • GMP extension (recommended for better performance)
  • simplesoftwareio/simple-qrcode (optional, for QR code image generation)

Installation

composer require corecave/laravel-zatca

Publish the configuration file:

php artisan vendor:publish --tag=zatca-config

Run the migrations:

php artisan migrate

Optional: QR Code Image Generation

To generate QR code images (PNG/SVG) for embedding in emails or PDFs:

composer require simplesoftwareio/simple-qrcode

Configuration

Add these environment variables to your .env file:

# Environment: sandbox, simulation, or production
ZATCA_ENVIRONMENT=simulation

# ===========================================
# Seller Information
# ===========================================
ZATCA_SELLER_NAME="Your Company Name"
ZATCA_SELLER_NAME_AR="اسم شركتك بالعربي"

# VAT Number: 15 digits, format: 3XXXXXXXXXX0003
# - Must start with 3
# - Followed by 10-digit registration number
# - Followed by 0003
ZATCA_VAT_NUMBER=310000000000003

# Commercial Registration Number (CRN): 10 digits
ZATCA_REGISTRATION_NUMBER=1000000000

# ===========================================
# Seller Address (Required for SA)
# ===========================================
ZATCA_SELLER_STREET="Main Street"
ZATCA_SELLER_BUILDING="1234"           # 4 digits
ZATCA_SELLER_CITY="Riyadh"
ZATCA_SELLER_DISTRICT="Al Olaya"
ZATCA_SELLER_POSTAL_CODE="12345"       # 5 digits
ZATCA_SELLER_ADDITIONAL_NUMBER="1234"  # 4 digits (optional)

# ===========================================
# CSR Configuration
# ===========================================
ZATCA_CSR_ORGANIZATION="Your Company Name"
ZATCA_CSR_ORGANIZATION_UNIT="Main Branch"
# Common name format depends on environment:
# - Simulation: TST-886431145-{VAT_NUMBER}
# - Production: {VAT_NUMBER}
ZATCA_CSR_COMMON_NAME="TST-886431145-310000000000003"

# Invoice types: 1100 = B2B + B2C, 1000 = B2C only, 0100 = B2B only
ZATCA_INVOICE_TYPES=1100

# Business category
ZATCA_BUSINESS_CATEGORY="Retail"
ZATCA_CITY="Riyadh"

# ===========================================
# Debug (optional, for development)
# ===========================================
ZATCA_DEBUG_ENABLED=true
ZATCA_DEBUG_PATH=zatca/debug

Understanding ZATCA Environments

Environment Portal API Endpoint Purpose
sandbox Developer Portal /developer-portal Basic development testing with mock certificates
simulation Simulation Portal /simulation Real testing with ZATCA - invoices are validated but not recorded
production FATOORA Portal /core Live production - invoices are legally binding

Important Notes:

  • Sandbox uses mock certificates that won't pass ZATCA validators - only for initial development
  • Simulation uses real ZATCA certificates but invoices aren't recorded - use for integration testing
  • Production is live - every invoice submitted is legally binding

Complete Onboarding Process

Overview

The onboarding process involves 4 steps:

  1. Generate CSR - Create a Certificate Signing Request
  2. Get Compliance CSID - Submit CSR with OTP to get a compliance certificate
  3. Pass Compliance Checks - Submit sample invoices for validation
  4. Get Production CSID - Exchange compliance certificate for production certificate

Step 1: Generate CSR

php artisan zatca:generate-csr --save

This generates:

  • A private key (stored securely)
  • A CSR file for submission to ZATCA

Step 2: Get Compliance CSID & Run Compliance Checks

  1. Log in to the appropriate ZATCA portal:

  2. Navigate to your EGS (e-Invoice Generation Solution) unit

  3. Generate a new OTP (One-Time Password)

  4. Run the compliance command within 1 hour (OTP expires):

# For simulation environment
php artisan zatca:compliance --otp=123456

# The command will:
# 1. Submit your CSR to ZATCA
# 2. Receive a compliance certificate (CSID)
# 3. Run compliance checks with sample invoices
# 4. Display pass/fail status for each check

Step 3: Get Production CSID

After ALL compliance checks pass, request your production certificate:

php artisan zatca:production-csid

Important:

  • This command does NOT require a new OTP
  • It uses your compliance certificate to authenticate
  • The compliance request ID is used to verify you passed compliance
  • Only works if you completed Step 2 successfully

Step 4: Start Issuing Invoices

Once you have a production certificate, you can start issuing legally-binding invoices:

use Corecave\Zatca\Facades\Zatca;

// The package automatically uses your production certificate
$result = Zatca::process($invoice);

Full Usage Example

Here's a complete example from building an invoice to submitting it:

<?php

namespace App\Services;

use Corecave\Zatca\Facades\Zatca;
use Corecave\Zatca\Invoice\InvoiceBuilder;
use Corecave\Zatca\Enums\VatCategory;
use Corecave\Zatca\Enums\PaymentMethod;
use Corecave\Zatca\Models\ZatcaInvoice;

class InvoiceService
{
    /**
     * Create and submit a B2C invoice to ZATCA.
     */
    public function createSimplifiedInvoice(array $orderData): ZatcaInvoice
    {
        // Step 1: Build the invoice
        $invoice = InvoiceBuilder::simplified()
            ->setInvoiceNumber('INV-' . date('Y') . '-' . str_pad($orderData['id'], 6, '0', STR_PAD_LEFT))
            ->setIssueDate(now())
            ->setSupplyDate(now())
            ->setPaymentMethod(PaymentMethod::CASH);

        // Step 2: Add line items
        foreach ($orderData['items'] as $item) {
            $invoice->addLineItem([
                'name' => $item['name'],
                'quantity' => $item['quantity'],
                'unit_price' => $item['price'],  // Price EXCLUDING VAT
                'vat_category' => VatCategory::STANDARD,  // 15% VAT
            ]);
        }

        // Step 3: Build and submit to ZATCA
        $builtInvoice = $invoice->build();
        $result = Zatca::report($builtInvoice);  // B2C uses report()

        // Step 4: Handle the result
        if ($result->isSuccess()) {
            // Get the stored invoice record
            $zatcaInvoice = ZatcaInvoice::where('uuid', $builtInvoice->getUuid())->first();

            // QR code for printing on receipt
            $qrCodeTlv = $result->getQrCode();

            // QR code as PNG for embedding in emails/PDFs
            $qrCodePng = $zatcaInvoice->qr_code_image;

            return $zatcaInvoice;
        }

        // Handle errors
        throw new \Exception('ZATCA submission failed: ' . json_encode($result->getErrors()));
    }

    /**
     * Create and submit a B2B invoice to ZATCA.
     */
    public function createStandardInvoice(array $orderData, array $buyerData): ZatcaInvoice
    {
        // Step 1: Build the invoice with buyer information
        $invoice = InvoiceBuilder::standard()
            ->setInvoiceNumber('INV-' . date('Y') . '-' . str_pad($orderData['id'], 6, '0', STR_PAD_LEFT))
            ->setIssueDate(now())
            ->setSupplyDate(now())
            ->setPaymentMethod(PaymentMethod::CREDIT)
            ->setBuyer([
                'name' => $buyerData['company_name'],
                'vat_number' => $buyerData['vat_number'],
                'registration_number' => $buyerData['cr_number'],
                'registration_scheme' => 'CRN',
                'address' => [
                    'street' => $buyerData['street'],
                    'building' => $buyerData['building'],
                    'city' => $buyerData['city'],
                    'district' => $buyerData['district'],
                    'postal_code' => $buyerData['postal_code'],
                    'country' => 'SA',
                ],
            ]);

        // Step 2: Add line items
        foreach ($orderData['items'] as $item) {
            $invoice->addLineItem([
                'name' => $item['name'],
                'quantity' => $item['quantity'],
                'unit_price' => $item['price'],
                'vat_category' => VatCategory::STANDARD,
            ]);
        }

        // Step 3: Build and submit to ZATCA
        $builtInvoice = $invoice->build();
        $result = Zatca::clear($builtInvoice);  // B2B uses clear()

        // Step 4: Handle the result
        if ($result->isSuccess()) {
            return ZatcaInvoice::where('uuid', $builtInvoice->getUuid())->first();
        }

        throw new \Exception('ZATCA clearance failed: ' . json_encode($result->getErrors()));
    }

    /**
     * Auto-detect invoice type and submit.
     */
    public function submitInvoice($invoice): ZatcaInvoice
    {
        // process() automatically uses report() for B2C and clear() for B2B
        $result = Zatca::process($invoice);

        if ($result->wasReported()) {
            // B2C invoice was reported
        }

        if ($result->wasCleared()) {
            // B2B invoice was cleared
        }

        return ZatcaInvoice::where('uuid', $invoice->getUuid())->first();
    }
}

VAT Number & Registration Number Formats

VAT Number (15 digits)

Format: 3XXXXXXXXXX0003

Position Value Description
1 3 Always 3 (country code for Saudi Arabia)
2-11 XXXXXXXXXX Your 10-digit Commercial Registration Number
12-15 0003 Fixed suffix

Example: If your CR is 1234567890, your VAT number is 312345678900003

Commercial Registration Number (10 digits)

This is your company's official registration number from the Ministry of Commerce.

Example: 1234567890

Buyer Identification Schemes

When specifying buyer information for B2B invoices, use the appropriate scheme:

Scheme ID Description Format
CRN Commercial Registration Number 10 digits
MOM Momra License Variable
MLS MLSD License Variable
SAG Sagia License Variable
NAT National ID 10 digits
GCC GCC ID Variable
IQA Iqama Number 10 digits
TIN Tax Identification Number (VAT) 15 digits
700 700 Number Variable
OTH Other ID Variable

Creating Credit Notes (Refunds)

use Corecave\Zatca\Invoice\InvoiceBuilder;

// Credit note for a B2C refund
$creditNote = InvoiceBuilder::creditNote(simplified: true)
    ->setInvoiceNumber('CN-2024-001')
    ->setOriginalInvoice('INV-2024-001')  // Reference the original invoice
    ->setReason('Customer returned goods')
    ->addLineItem([
        'name' => 'Returned Product',
        'quantity' => 1,
        'unit_price' => 100.00,
        'vat_category' => VatCategory::STANDARD,
    ])
    ->build();

$result = Zatca::process($creditNote);

QR Code Image Generation

Generate QR code images for receipts, emails, or PDFs:

use Corecave\Zatca\Models\ZatcaInvoice;

$invoice = ZatcaInvoice::find($id);

// Get QR code as base64-encoded PNG (for emails/PDFs)
$pngBase64 = $invoice->qr_code_image;
echo '<img src="data:image/png;base64,' . $pngBase64 . '" alt="QR Code">';

// Get QR code as SVG (for web display)
$svg = $invoice->qr_code_svg;
echo $svg;

// Get raw TLV data (for custom QR generation)
$tlvData = $invoice->qr_code;

Note: Requires simplesoftwareio/simple-qrcode package.

Artisan Commands

Generate CSR

php artisan zatca:generate-csr --save

Run Compliance Process

# Get OTP from ZATCA portal first, then:
php artisan zatca:compliance --otp=123456

Get Production CSID

# After compliance passes (no OTP needed):
php artisan zatca:production-csid

# Or specify request ID manually:
php artisan zatca:production-csid --request-id=1234567890

Renew Production Certificate

# Get new OTP from ZATCA portal, then:
php artisan zatca:renew-csid --otp=123456

# Force renewal even if not expiring:
php artisan zatca:renew-csid --otp=123456 --force

Cleanup Utility

# Show help
php artisan zatca:cleanup

# Clean up compliance certificates
php artisan zatca:cleanup --compliance

# Clean up production certificates
php artisan zatca:cleanup --production

# Clean up all certificates
php artisan zatca:cleanup --certificates

# Clean up CSR and private key files
php artisan zatca:cleanup --csr

# Clean up all invoices from database
php artisan zatca:cleanup --invoices

# Clean up debug files
php artisan zatca:cleanup --debug

# Clean up everything
php artisan zatca:cleanup --all

# Skip confirmation prompts
php artisan zatca:cleanup --all --force

Debugging

Enable debug mode to save XML files for inspection:

ZATCA_DEBUG_ENABLED=true
ZATCA_DEBUG_PATH=zatca/debug

Debug files are saved to storage/app/zatca/debug/:

  • {invoice}_unsigned.xml - XML before signing
  • {invoice}_signed.xml - XML after signing
  • {invoice}_hash.txt - Invoice hash
  • {invoice}_qr.txt - QR code TLV data

Error Handling

use Corecave\Zatca\Exceptions\ApiException;
use Corecave\Zatca\Exceptions\ValidationException;
use Corecave\Zatca\Exceptions\CertificateException;

try {
    $result = Zatca::report($invoice);

    if (!$result->isSuccess()) {
        // ZATCA accepted but with warnings
        $warnings = $result->getWarnings();
    }
} catch (ValidationException $e) {
    // Invoice validation failed locally
    $errors = $e->getErrors();
} catch (ApiException $e) {
    // ZATCA API returned an error
    $zatcaErrors = $e->getZatcaErrors();
    $zatcaWarnings = $e->getZatcaWarnings();
} catch (CertificateException $e) {
    // Certificate issue (missing, expired, invalid)
    $message = $e->getMessage();
}

Common ZATCA Validation Errors

Error Code Message Solution
BR-KSA-F-13 Invalid Seller/Buyer ID Check VAT number format (15 digits: 3XXXXXXXXXX0003)
BR-KSA-63 Missing buyer address fields Include all required fields for SA buyers
BR-KSA-18 Invalid building number Building number must be exactly 4 digits
BR-KSA-64 Invalid additional number Additional number must be exactly 4 digits
X509IssuerName Wrong certificate issuer Use simulation/production environment, not sandbox
Invalid-CSR CSR is invalid Regenerate CSR with correct configuration

Events

The package dispatches events you can listen to:

// In EventServiceProvider
protected $listen = [
    \Corecave\Zatca\Events\InvoiceReported::class => [
        \App\Listeners\HandleInvoiceReported::class,
    ],
    \Corecave\Zatca\Events\InvoiceCleared::class => [
        \App\Listeners\HandleInvoiceCleared::class,
    ],
    \Corecave\Zatca\Events\InvoiceRejected::class => [
        \App\Listeners\HandleInvoiceRejected::class,
    ],
];

Changelog

v1.2.0 (2024-12-16)

Added

  • QR code image generation (qr_code_image and qr_code_svg attributes)
  • Optional dependency on simplesoftwareio/simple-qrcode

v1.1.0 (2024-12-13)

Fixed

  • X509IssuerName format matching ZATCA SDK
  • SignedProperties hash computation
  • Buyer PartyIdentification with proper schemeID
  • SA buyer address fields (BuildingNumber, District, etc.)

Added

  • Compliance check command with sample invoice generation
  • Certificate cleanup command
  • Automatic schemeID detection

v1.0.0 (2024-12-12)

  • Initial release with full Phase 2 support

License

MIT License. See LICENSE for more information.

Resources