Skip to content

Improve Factur-X compliance for French PDP validation#12

Open
ThierryHFR wants to merge 1 commit into
InvoicePlane:developfrom
ThierryHFR:facturx-france-pdp-compliance
Open

Improve Factur-X compliance for French PDP validation#12
ThierryHFR wants to merge 1 commit into
InvoicePlane:developfrom
ThierryHFR:facturx-france-pdp-compliance

Conversation

@ThierryHFR

@ThierryHFR ThierryHFR commented Jun 11, 2026

Copy link
Copy Markdown
  • Use EN16931 profile identifier compatible with French validators
  • Sanitize invoice references according to BR-FR-02 requirements
  • Add mandatory French legal notes (PMT, PMD, AAB)
  • Export seller SIREN from user_siren
  • Export buyer routing identifier from client_company
  • Add BT-34 and BT-49 electronic addresses using SIREN scheme (0002)
  • Generate valid BT-30 seller identification
  • Prevent invalid payment means generation when no IBAN/account identifier is available
  • Fix BR-50, BR-61 and BR-CO-27 validation issues
  • Validate generated invoices with SuperPDP and FactPulse

Pull Request Checklist

Please check the following steps before submitting your PR. If any items are incomplete, consider marking it as [WIP] (Work in Progress).

Checklist

  • My code follows the code formatting guidelines.
  • I have tested my changes locally.
  • I selected the appropriate branch for this PR.
  • I have rebased my changes on top of the selected branch.
  • I included relevant documentation updates if necessary.
  • I have an accompanying issue ID for this pull request.

Description

Provide a brief description of the changes made in this pull request.


Related Issue(s)

List any related issues or discussions. If applicable, link to an accompanying thread on the forums.
Fixes # (example)


Motivation and Context

See #11


Issue Type (Check one or more)

  • Bugfix
  • Improvement of an existing feature
  • New feature

Screenshots (If Applicable)

Attach relevant screenshots that demonstrate your changes.


Thank you for your contribution to InvoicePlane! We appreciate your time and effort.

- Use EN16931 profile identifier compatible with French validators
- Sanitize invoice references according to BR-FR-02 requirements
- Add mandatory French legal notes (PMT, PMD, AAB)
- Export seller SIREN from user_siren
- Export buyer routing identifier from client_company
- Add BT-34 and BT-49 electronic addresses using SIREN scheme (0002)
- Generate valid BT-30 seller identification
- Prevent invalid payment means generation when no IBAN/account identifier is available
- Fix BR-50, BR-61 and BR-CO-27 validation issues
- Validate generated invoices with SuperPDP and FactPulse
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR updates Factur-X v1.0 invoice generation to enforce French PDP compliance. Configuration changes remap Factur-X extended URNs to base EN16931. New helper methods sanitize SIREN identifiers and normalize invoice references. XML generation routes seller parties through SIREN with schemeID 0002, injects mandatory French legal notes, and conditionalizes payment means output based on available account data.

Changes

French PDP Compliance for Factur-X Invoicing

Layer / File(s) Summary
Helper methods and URN configuration
application/helpers/XMLconfigs/Facturxv10extended.php, application/libraries/XMLtemplates/Facturxv10Xml.php (lines 35–44, 87–91)
Configuration baseline updates URN to urn:cen.eu:en16931:2017. Helper methods cleanedSiren() and invoiceReference() sanitize SIREN to digits-only and normalize invoice reference for French validation. URN remapping redirects extended Factur-X URNs to base EN16931 for PDP compatibility.
Document structure and normalized invoice reference
application/libraries/XMLtemplates/Facturxv10Xml.php (lines 109–128)
Exchanged document ram:ID uses normalized invoice reference instead of raw invoice number. Mandatory French legal notes (PMT/PMD/AAB) are injected as ram:IncludedNote entries with fixed subject codes and content.
Seller party structure with SIREN and legal organization
application/libraries/XMLtemplates/Facturxv10Xml.php (lines 207–226)
Seller party routing uses cleaned SIREN for ram:ID and builds ram:SpecifiedLegalOrganization/ram:ID with schemeID="0002", replacing prior generic ID/EAS/tax-code logic.
Party communication identifiers using SIREN
application/libraries/XMLtemplates/Facturxv10Xml.php (lines 271–286)
URIUniversalCommunication block replaced with French PDP-specific logic: always emits ram:URIID using cleaned SIREN values and schemeID="0002" for both seller and buyer.
Settlement and payment means generation
application/libraries/XMLtemplates/Facturxv10Xml.php (lines 332–343, 483–500)
Settlement ram:PaymentReference uses normalized invoice reference. Payment means inclusion is conditional on helper returning non-null. Payment account generation prefers IBAN, falls back to bank account as ProprietaryID, and returns null when both are missing to avoid invalid empty output.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • Factur-X / EN16931 #11: Both PRs implement French PDP compliance for Factur-X EN16931 URN handling, SIREN identifier routing, invoice-reference normalization, and payment-means generation logic.

Poem

🐰 A SIREN calls through PDP's gate,
URNs align and invoice references normalize straight,
IBAN flows or bank accounts stand,
French legal notes firmly in hand! 🇫🇷✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improve Factur-X compliance for French PDP validation' directly and clearly summarizes the main objective of the changeset: enhancing Factur-X XML configuration and library code to meet French PDP (Portail de la Dématérialisation Publique) validation requirements through updates to URN handling, invoice references, SIREN identifiers, and legal notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@application/libraries/XMLtemplates/Facturxv10Xml.php`:
- Around line 209-224: Ensure we don't emit empty/invalid SIREN-derived
identifiers: before calling $this->cleanedSiren($this->invoice->user_siren ??
'') and appending ram:ID / ram:URIID nodes (the code that creates
$node->appendChild(... 'ram:ID'), $idNode = $this->doc->createElement('ram:ID',
...), and any ram:URIID creation), validate the result is a strict 9-digit SIREN
(digits only, length == 9); if validation fails, either throw an explicit
exception (fail fast) or use a clear fallback behavior (do not create the
ram:ID/ram:URIID node and/or log/error) so invalid empty nodes are never
emitted; apply this check wherever cleanedSiren() is used for seller SIREN
(including the SpecifiedLegalOrganization block that creates $sloNode and
$idNode and the code path using $this->options[$prop[9]]).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c202fa2-88ab-4776-ae3e-cd812a61d5cb

📥 Commits

Reviewing files that changed from the base of the PR and between 9c9fd8c and a2bd056.

📒 Files selected for processing (2)
  • application/helpers/XMLconfigs/Facturxv10extended.php
  • application/libraries/XMLtemplates/Facturxv10Xml.php

Comment on lines 209 to +224
if (empty($this->minimum) && $who == 'user') {
$node->appendChild($this->doc->createElement('ram:ID', $this->invoice->{$prop[0]})); // *_id zugferd 2 : SELLER123
$sellerSiren = $this->cleanedSiren($this->invoice->user_siren ?? '');
$node->appendChild($this->doc->createElement('ram:ID', $sellerSiren));
}

$node->appendChild($this->doc->createElement('ram:Name', htmlsc($this->invoice->{$prop[1]}))); // *_name

// SpecifiedLegalOrganization XRechnung-CII-validation + (minimum profile : need for user if $this->notax)
if ( ! empty($this->options[$prop[9]])) { // *_eas_code
// Required when "No subject to VAT" for minimum profile (Factur-X/Zugferd2.3).
// Note: is valid with VAT but not required
// Only for MINIMUM profile : ram:SpecifiedLegalOrganization is expected for seller (user) only
// SpecifiedLegalOrganization
// For France, FactPulse / XP Z12-012 expects seller SIREN in BT-30: exactly 9 digits.
if ($who == 'user') {
$sloNode = $this->doc->createElement('ram:SpecifiedLegalOrganization');
$idNode = $this->doc->createElement('ram:ID', $this->cleanedSiren($this->invoice->user_siren ?? ''));
$idNode->setAttribute('schemeID', '0002');
$sloNode->appendChild($idNode);
$node->appendChild($sloNode);
} elseif ( ! empty($this->options[$prop[9]])) { // *_eas_code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent empty SIREN-derived identifiers from being emitted.

Line 210, Line 220, and Line 282 can write empty values after cleanedSiren() (e.g., missing/non-numeric source fields), which generates invalid ram:ID / ram:URIID nodes and breaks PDP validation contracts (BT-30/BT-34/BT-49). Validate a strict 9-digit SIREN before emitting these nodes, and fail fast (or apply explicit fallback) when unavailable.

Suggested patch
+        $sellerSiren = $this->cleanedSiren($this->invoice->user_siren ?? '');
+        $hasSellerSiren = preg_match('/^\d{9}$/', $sellerSiren) === 1;
+
-        if (empty($this->minimum) && $who == 'user') {
-            $sellerSiren = $this->cleanedSiren($this->invoice->user_siren ?? '');
+        if (empty($this->minimum) && $who === 'user' && $hasSellerSiren) {
             $node->appendChild($this->doc->createElement('ram:ID', $sellerSiren));
         }

-        if ($who == 'user') {
+        if ($who === 'user' && $hasSellerSiren) {
             $sloNode = $this->doc->createElement('ram:SpecifiedLegalOrganization');
-            $idNode  = $this->doc->createElement('ram:ID', $this->cleanedSiren($this->invoice->user_siren ?? ''));
+            $idNode  = $this->doc->createElement('ram:ID', $sellerSiren);
             $idNode->setAttribute('schemeID', '0002');
             $sloNode->appendChild($idNode);
             $node->appendChild($sloNode);
         } elseif ( ! empty($this->options[$prop[9]])) { // *_eas_code
@@
-        $uriNode = $this->doc->createElement('ram:URIUniversalCommunication');
-        $idNode = $this->doc->createElement('ram:URIID', $electronicAddress);
-        $idNode->setAttribute('schemeID', '0002');
-        $uriNode->appendChild($idNode);
-        $node->appendChild($uriNode);
+        if ($electronicAddress !== '') {
+            $uriNode = $this->doc->createElement('ram:URIUniversalCommunication');
+            $idNode = $this->doc->createElement('ram:URIID', $electronicAddress);
+            $idNode->setAttribute('schemeID', '0002');
+            $uriNode->appendChild($idNode);
+            $node->appendChild($uriNode);
+        }

Also applies to: 271-285

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/libraries/XMLtemplates/Facturxv10Xml.php` around lines 209 - 224,
Ensure we don't emit empty/invalid SIREN-derived identifiers: before calling
$this->cleanedSiren($this->invoice->user_siren ?? '') and appending ram:ID /
ram:URIID nodes (the code that creates $node->appendChild(... 'ram:ID'), $idNode
= $this->doc->createElement('ram:ID', ...), and any ram:URIID creation),
validate the result is a strict 9-digit SIREN (digits only, length == 9); if
validation fails, either throw an explicit exception (fail fast) or use a clear
fallback behavior (do not create the ram:ID/ram:URIID node and/or log/error) so
invalid empty nodes are never emitted; apply this check wherever cleanedSiren()
is used for seller SIREN (including the SpecifiedLegalOrganization block that
creates $sloNode and $idNode and the code path using $this->options[$prop[9]]).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant