Skip to content

About

Monorepo for hamada-emam-tech/einvoice: one PHP interface for tax-authority e-invoicing (ZATCA, Egypt ETA). Core + one package per authority, pluggable signing keys.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

hamada-emam-tech/einvoice (monorepo)

This repo holds the core and every authority and key-source package. Each folder in packages/ is split into its own read-only repo by .github/workflows/split.yml and published on Packagist separately.

One PHP interface for tax-authority e-invoicing: sign every invoice and submit it. Install the core, then only the authority you need.

composer require hamada-emam-tech/einvoice           # the core: model, software keys, a fake authority for tests
composer require hamada-emam-tech/einvoice-zatca     # Saudi ZATCA (Fatoora)
composer require hamada-emam-tech/einvoice-eta       # Egypt ETA
composer require hamada-emam-tech/einvoice-keysource-agent  # USB token or HSM, via signing-agent

Installing an authority package is enough. The core finds it automatically.

Status: the core ships SoftwareKey and the fake authority. ZATCA is available in einvoice-zatca, tested live against ZATCA's developer portal. Egypt ETA is in einvoice-eta, checked against ETA's SDK samples and with OpenSSL; its pre-production test is still to run. You can already write your own with extend() (see writing an authority or key source).

Two separate choices: the authority and the key

The authority decides the document format and the API: UBL XML for ZATCA, canonical JSON for ETA. The key source decides where the private key lives. The authority never knows which one it got.

Key source Where the key lives Package
SoftwareKey A PEM file or secret your app holds (ZATCA) in the core
Signing Agent A USB token or HSM, through PKCS#11 and the local signing-agent (Egypt ETA) einvoice-keysource-agent
CSC A cloud signing service (CSC API) einvoice-keysource-csc (planned)

Use it

use HamadaEmamTech\Einvoice\Einvoice;
use HamadaEmamTech\Einvoice\Model\{Address, Invoice, Line, Money, Party};
use HamadaEmamTech\Einvoice\Signing\SoftwareKey;

$einvoice = new Einvoice([
    'default'     => 'zatca',
    'authorities' => [
        'zatca' => [
            'environment' => 'sandbox',
            'certificate' => getenv('ZATCA_CSID_TOKEN'),   // from onboarding, see einvoice-zatca
            'secret'      => getenv('ZATCA_CSID_SECRET'),
            // A KeySource, or a callable returning one, so the key is only read when signing.
            'key' => fn () => SoftwareKey::fromFile('/secrets/egs.key', getenv('EGS_KEY_PASS'), '/secrets/csid.pem'),
        ],
    ],
]);

$invoice = new Invoice(
    number:   'INV-1001',
    issuedAt: new DateTimeImmutable(),
    seller:   new Party('Seller Co', '300000000000003',
                  new Address('SA', 'Riyadh', 'King Fahd Road', '1234', 'Al Olaya', '12345'),
                  identifiers: ['CRN' => '1010010000']),
    buyer:    new Party('Buyer Co', '311111111111113',
                  new Address('SA', 'Riyadh', 'Olaya Street', '2222', 'Al Olaya', '12222')),
    lines:    [Line::taxed('Widget', '3', Money::of('10.00', 'SAR'), 'S', '15')],
    currency: 'SAR',
    extra:    ['zatca' => ['icv' => 1001, 'previous_hash' => $lastHash]],  // authority-specific fields
);

$signed = $einvoice->sign($invoice);    // prepare + sign. Store $signed before you submit.
$result = $einvoice->submit($signed);   // ideally from a queue, with retries

Signing for many tenants or devices? Pass the key in: $einvoice->sign($invoice, $tenantKey).

Money is never a float

Amounts are integer minor units plus a currency. Parse them from strings:

Money::of('115.50', 'SAR');            // 11550 halalas
Money::of('1.234', 'KWD');             // 3-decimal currencies are handled
Money::of('10.005', 'SAR');            // throws: round it yourself, nothing is rounded silently
Money::of('100.00', 'SAR')->percent('15');  // 15.00, half away from zero

A float changes the canonical string, and one changed character breaks the signature.

Partial success is normal

One submission can come back with accepted and rejected documents together (Egypt ETA does this). A rejected document is data, not an exception:

foreach ($result->rejected as $rejected) {
    foreach ($rejected->errors as $problem) {
        log("{$rejected->invoiceNumber}: [{$problem->code}] {$problem->message}");
    }
}

AuthorityError is thrown only when the call itself fails: authentication, an outage, a malformed request.

Keys

$key = SoftwareKey::generate('secp256k1');                 // ZATCA uses secp256k1
file_put_contents('egs.key', $key->exportPrivateKey($passphrase));

$key = SoftwareKey::fromFile('egs.key', $passphrase)->withCertificate($csidPem);

SoftwareKey stays out of var_dump(), dd() and error trackers.

Test without a real authority

$einvoice = new Einvoice(['default' => 'fake']);
$fake = $einvoice->authority();                  // HamadaEmamTech\Einvoice\Testing\FakeAuthority
$fake->reject('INV-2', 'Buyer VAT number is invalid');

$result = $einvoice->issue($invoice);
$fake->assertSubmitted(fn ($doc) => $doc->invoiceNumber === 'INV-1001');

FakeKeySource signs anything and remembers it: $key->assertSigned().

Authorities

Package Authority Key Status
hamada-emam-tech/einvoice-zatca ZATCA, Saudi Arabia (Fatoora) software key, secp256k1 available
hamada-emam-tech/einvoice-eta ETA, Egypt RSA certificate (USB token, HSM or PEM), CAdES-BES built; pre-production test pending

Framework-agnostic: PHP 8.2+ with ext-openssl, no framework dependency.

About

Monorepo for hamada-emam-tech/einvoice: one PHP interface for tax-authority e-invoicing (ZATCA, Egypt ETA). Core + one package per authority, pluggable signing keys.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages