Skip to content

[Core]: Certification with Portuguese TAX Authority (SAF-T) #407

Open

Description

@spacecabbie

In Portugal its required to have any invoicing system to be certified by the tax authority.

Context (Environment)

Since 2021 Its mandated to as a company make invoices this system has to be certified by AT (tax office) for this certication it has to comply with certain specifications. This request is to look if its feasable to do this for invoiceplane (see: https://community.invoiceplane.com/t/topic/10457/3)

Detailed Description

What are the requirements for certification of invoicing programs?

For an invoicing program to be certified by the Tax Authority, it must meet the following requirements:

  • Possibility of exporting the tax audit model file (as defined by Ordinance no. 321-A/2007, of March 26); (for example, SAF-T );
  • Have a system that allows the recording of records of invoices or equivalent documents and sales receipts to be identified, using an asymmetric cipher algorithm and a private key known exclusively to the program producer;
  • Have access control to the computer system, requiring authentication for each user;
  • Do not have any function that, locally or remotely, allows you to change, directly or indirectly, information of a tax nature, without generating evidence added to the original information.
  • Comply with the technical requirements approved by order of the director general of the Tax and Customs Authority (AT).
  • Public key and private key used by the program to be certified.

References:

  1. https://eportugal.gov.pt/inicio/espaco-empresa/balcao-do-empreendedor/programa-de-faturacao-certificacao
  2. https://www.voxelgroup.net/blog/en/portugal-2023-main-changes-to-electronic-invoicing/

The application to get certified has to be done by a Portugese company locally if wanted i am willing to do this.

Activity

  1. changed the title [-]Feature Request: Certification with Portugese TAX Authority.[/-] [+][Feature Request]: Certification with Portugese TAX Authority.[/+] on Dec 17, 2023
  2. changed the title [-][Feature Request]: Certification with Portugese TAX Authority.[/-] [+][Feature Request] Certification with Portugese TAX Authority.[/+] on Dec 17, 2023
  3. Verony-makesIT commented on Dec 18, 2023

    @Verony-makesIT

    @nielsdrost7 & @spacecabbie,
    For your information: link to the Portuguese EU e-invoicing page.

    Companies will have to fulfill their VAT obligations by sending their (electronic) invoices to the VAT authorities, possibly with the help of "a certified partner".
    In my opinion, InvoicePlane is an application for (small) companies that creates and sends (pdf and maybe later UBL or CII) invoices to their customers and not an application that (should also) help companies comply with these (national/Portuguese) VAT obligations.

  4. ZanzyTHEbar commented on Aug 27, 2025

    @ZanzyTHEbar
    Collaborator

    Hey! Would love to use Invoice plane, but live in Portugal. I am developer as well - what is the status on this, and are you looking for help?

  5. nielsdrost7 commented on Aug 27, 2025

    @nielsdrost7
    Collaborator

    Hey @ZanzyTHEbar,
    come find me on Discord, https://discord.gg/PPzD2hTrXt
    I'll fill you in.

  6. ZanzyTHEbar commented on Aug 30, 2025

    @ZanzyTHEbar
    Collaborator

    Hey @ZanzyTHEbar, come find me on Discord, https://discord.gg/PPzD2hTrXt I'll fill you in.

    Done. Send me a dm.

  7. changed the title [-][Feature Request] Certification with Portugese TAX Authority.[/-] [+][Core]: Certification with Portuguese TAX Authority (SAF-T)[/+] on Jun 13, 2026
  8. nielsdrost7 commented on Jun 13, 2026

    @nielsdrost7
    Collaborator

    Planning Note — v2 Implementation

    Story
    As a Portuguese business owner using InvoicePlane, I want to export a SAF-T (PT) XML file and have the application meet AT certification requirements so that I can legally use the software for invoicing in Portugal.

    v2 Context
    This is a significant compliance feature with two distinct workstreams:

    1. SAF-T XML export — generating the AuditFile XML per Portaria 321-A/2007 (and subsequent updates). This maps directly to existing v2 models: Invoice, InvoiceItem, Client (Relation model), Payment, TaxRate, Company.
    2. AT certification — a formal process requiring: digital signing of invoice hashes with an asymmetric key (RSA), sequential hash chaining across invoices, and submission of a public key to AT. This touches the Invoice creation flow at a deep level and must not break existing invoice generation.

    Both workstreams can be built incrementally. The SAF-T export is self-contained; the hash/signing requirement changes how invoices are persisted.

    Acceptance Criteria

    SAF-T Export

    • A SaftExportService produces a valid SAF-T PT XML file for a given company and date range
    • The XML conforms to the official AT schema (SAFTPT1.04_01.xsd or current version)
    • All required sections are present: Header, MasterFiles (customers, products, tax table), SourceDocuments (invoices, payments)
    • Export is triggerable via an Artisan command: php artisan saft:export {company} {--year=} {--month=}
    • Export is triggerable via a Filament action button in the Admin panel (Company settings or Reports section)
    • The exported file passes AT's online validator

    Invoice signing (AT certification requirement)

    • Each invoice record stores a hash field (SHA-256 of prior hash + date + doc no + gross total, signed with RSA-1024 private key, Base64-encoded)
    • Hash is computed and stored at invoice creation time via an Observer or Service method
    • The private key is stored outside the webroot (configurable via config/ip.php)
    • The public key can be exported for submission to AT
    • No existing invoice data can be modified without generating an audit record

    Implementation

    • Module: Modules/Invoices/ (primary) + Modules/Core/ (config, command registration)
    • Key files to create/modify:
      • Modules/Invoices/Services/SaftExportService.php — core XML builder; injects Company, date range; returns XML string
      • Modules/Invoices/Services/InvoiceHashService.php — RSA signing logic; reads private key from config path
      • Modules/Invoices/Observers/InvoiceObserver.php — calls InvoiceHashService on creating
      • database/migrations (Invoices module) — add hash VARCHAR(172), hash_control VARCHAR(172) columns to invoices table
      • Modules/Invoices/Console/SaftExportCommand.php — Artisan command
      • Modules/Invoices/Filament/Company/Resources/InvoiceResource/Pages/ExportSaft.php — Filament page or action
      • config/ip.php — add saft.private_key_path, saft.company_at_code settings
      • Modules/Invoices/Enums/InvoiceType.php — ensure types map to AT document type codes (FT, FR, ND, NC)

    Test Scenarios (PHPUnit)

    // Modules/Invoices/Tests/Unit/SaftExportServiceTest.php
    class SaftExportServiceTest extends AbstractTestCase
    {
        #[Test]
        public function test_export_produces_valid_xml_structure(): void
        {
            // arrange
            $company = Company::factory()->create();
            $invoice = Invoice::factory()->for($company)->has(InvoiceItem::factory()->count(2))->create();
    
            $service = new SaftExportService();
            $xml = $service->export($company, now()->startOfYear(), now()->endOfYear());
    
            // assert
            $dom = new \DOMDocument();
            $this->assertTrue($dom->loadXML($xml));
            $this->assertStringContainsString('<AuditFile', $xml);
            $this->assertStringContainsString('<Header>', $xml);
            $this->assertStringContainsString('<SourceDocuments>', $xml);
        }
    }
    
    // Modules/Invoices/Tests/Unit/InvoiceHashServiceTest.php
    class InvoiceHashServiceTest extends AbstractTestCase
    {
        #[Test]
        public function test_hash_is_deterministic_for_same_inputs(): void
        {
            $service = new InvoiceHashService(privateKeyPath: storage_path('test_key.pem'));
    
            $hash1 = $service->computeHash('prevHash', '2026-01-01', 'FT 1/1', '1000.00');
            $hash2 = $service->computeHash('prevHash', '2026-01-01', 'FT 1/1', '1000.00');
    
            $this->assertSame($hash1, $hash2);
        }
    
        #[Test]
        public function test_hash_changes_when_any_input_changes(): void
        {
            $service = new InvoiceHashService(privateKeyPath: storage_path('test_key.pem'));
    
            $hash1 = $service->computeHash('prevHash', '2026-01-01', 'FT 1/1', '1000.00');
            $hash2 = $service->computeHash('prevHash', '2026-01-01', 'FT 1/1', '1001.00');
    
            $this->assertNotSame($hash1, $hash2);
        }
    }
    
    // Modules/Invoices/Tests/Feature/SaftExportCommandTest.php
    class SaftExportCommandTest extends AbstractCompanyPanelTestCase
    {
        #[Test]
        public function test_artisan_command_creates_saft_file(): void
        {
            $this->artisan('saft:export', [
                'company' => $this->company->search_code,
                '--year'  => '2026',
            ])->assertExitCode(0);
        }
    }

    Open questions for the community

    • Which version of the SAFT-PT XSD schema should be targeted? (1.04_01 is current as of 2023)
    • RSA key length: AT specifies 1024-bit for the hash control field — confirm this has not been updated to 2048-bit
    • The formal AT certification application must be submitted by a Portuguese legal entity. @spacecabbie has offered to handle this step — this should be tracked as a separate non-code task once the technical implementation is complete.
  9. added this to the v2.0.0-alpha milestone on Jun 13, 2026
  10. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    Collaborator

    Test Suggestions

    Added to support future implementation of this request. The issue body is unchanged.

    There is no SAF-T or tax-authority-certification scaffolding in the codebase today. The closest existing feature is Peppol e-invoicing config in Modules/Invoices/Config/config.php ('peppol' => ['default_provider' => env('PEPPOL_PROVIDER', 'e_invoice_be'), ...]), which targets the Belgian e-invoice.be access point — a different standard from Portuguese SAF-T XML export and digital-signature requirements. A repo-wide search for signature/cipher logic (openssl_sign, private_key) finds nothing real (only false-positive matches on Artisan $signature command properties). This is a large compliance feature (asymmetric-key invoice signing, an audit-proof change log, and a specific XML export format) that would need its own module or a dedicated Modules/Core/Compliance area — the tests below sketch the SAF-T XML export piece only, since that's the most concretely scoped sub-requirement in the issue.

    • PHPUnit: it_exports_invoices_for_a_period_as_saft_compliant_xml()
    #[Test]
    public function it_exports_invoices_for_a_period_as_saft_compliant_xml(): void
    {
        /* Arrange */
        Invoice::factory()->for($this->company)->create(['invoiced_at' => now()]);
    
        /* Act */
        $xml = app(SafTExportService::class)->generate($this->company, now()->startOfMonth(), now()->endOfMonth());
    
        /* Assert */
        $this->assertStringContainsString('<AuditFile', $xml);
    }
    • PHPUnit: it_signs_each_invoice_hash_with_a_private_key_known_only_to_the_instance()
    #[Test]
    public function it_signs_each_invoice_hash_with_a_private_key_known_only_to_the_instance(): void
    {
        /* Arrange */
        $invoice = Invoice::factory()->for($this->company)->create();
    
        /* Act */
        $signature = app(InvoiceSigningService::class)->sign($invoice);
    
        /* Assert */
        $this->assertNotEmpty($signature);
    }
    • PHPUnit: it_generates_evidence_when_an_invoice_is_changed_after_signing()
    #[Test]
    public function it_generates_evidence_when_an_invoice_is_changed_after_signing(): void
    {
        /* Arrange */
        $invoice = Invoice::factory()->for($this->company)->create();
        app(InvoiceSigningService::class)->sign($invoice);
    
        /* Act */
        $invoice->update(['summary' => 'Changed after signing']);
    
        /* Assert */
        $this->assertDatabaseHas('audit_logs', ['auditable_id' => $invoice->id, 'auditable_type' => Invoice::class]);
    }

    Edge cases worth covering: this needs a legal/business decision (formal certification application via a Portuguese partner, per the reporter's own note) before implementation — the tests above assume the XML/signing mechanics only; certification itself is a process, not something PHPUnit can verify. Modules\Core\Models\AuditLog already exists and could underpin the "no undetected changes" requirement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions