Metadata-Version: 2.5
Name: facturseal-ci
Version: 0.0.2
Summary: Client de CI de FacturSeal : non-régression de conformité Factur-X / EN 16931
Project-URL: Homepage, https://facturseal.com
Project-URL: Documentation, https://facturseal.com/docs/ci
Project-URL: Source, https://gitlab.com/agapornis-ct/facturseal
Project-URL: Changelog, https://gitlab.com/agapornis-ct/facturseal/-/blob/main/clients/facturseal-ci/CHANGELOG.md
Author: Tsharp / FacturSeal
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: ci,en16931,factur-x,facturation-electronique,non-regression
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: hatchling>=1.24; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# facturseal-ci

Client de CI de [FacturSeal](https://facturseal.com) : il fait échouer votre pipeline
quand une facture qui était conforme à l'EN 16931 / Factur-X cesse de l'être.

```bash
pip install facturseal-ci
```

Aucune dépendance, aucun moteur, aucune image Docker : le paquet ne contient que le
strict nécessaire pour générer un corpus avec **votre** code, l'envoyer au service et
lire le verdict. Il s'installe en une seconde dans un runner.

## En une commande

```bash
export FACTURSEAL_URL=https://api.facturseal.com
export FACTURSEAL_TOKEN=$FS_TOKEN   # secret de projet

facturseal-ci \
  --generate "php examples/php/generate.php" \
  --corpus build/factures \
  --baseline facturseal-baseline.json \
  --report build/facturseal.md \
  --junit build/facturseal-junit.xml
```

Au premier run, la baseline de référence est écrite : **commitez-la**. Aux runs
suivants, elle est comparée au corpus régénéré.

## Codes de sortie

| Code | Signification |
|------|---------------|
| `0`  | Aucune régression retenue |
| `1`  | Régression retenue : une facture qui passait casse, le pipeline échoue |
| `2`  | Panne d'outil : service injoignable, corpus non validable, réponse illisible |

`2` n'est jamais `1`, et c'est le point : un service indisponible n'est pas une
facture non conforme. Une CI qui confondrait les deux enverrait votre équipe chercher
un bug dans son code de facturation.

## Le contrat de génération

`--generate` reçoit une commande, en **n'importe quel langage**. FacturSeal n'exécute
jamais votre code métier : il vous passe un scénario et attend une facture.

Pour chaque scénario servi par le service, votre commande est appelée avec :

- `$FS_SCENARIO_PAYLOAD` : chemin d'un fichier JSON décrivant la facture à produire,
  en termes métier BT/BG (jamais du XML) ;
- `$FS_INVOICE_OUT` : chemin où écrire la facture produite.

Elle doit sortir en `0` **si et seulement si** le fichier a été écrit.

```php
<?php
// Le connecteur, en entier. La seule pièce à écrire chez vous est la ligne du milieu.
$payload = json_decode(file_get_contents(getenv('FS_SCENARIO_PAYLOAD')), true);
$xml = MonEditeur\Facturation::genererFacturX($payload['blueprint']);
file_put_contents(getenv('FS_INVOICE_OUT'), $xml);
```

Un générateur de référence complet, en PHP nu (ni Composer ni framework), est publié
dans le dépôt d'exemples sous `examples/php/generate.php`, avec **sept payloads figés**
(`examples/payloads/`) qui permettent de développer et tester votre connecteur **hors
ligne**, sans jeton ni réseau : catégorie S, exonéré, autoliquidation, avoir, acompte,
multi-taux, remises au niveau document.

Le payload est un **`asdict` complet** : toutes les clés optionnelles sont présentes,
à `null`, `""` ou `[]`, et **les montants sont des chaînes** (`"240.00"`, jamais
`240.0`). Tester la présence d'une clé ne dit donc rien ; il faut tester sa valeur.

Le catalogue de scénarios est **servi par l'API** (`GET /me/scenarios`), pas embarqué
dans ce paquet : il évolue avec la réglementation sans que vous ayez à réinstaller
quoi que ce soit. `--cius core|fr-ctc` choisit la couche de juridiction servie ; sans
elle, l'instance sert sa couche de déploiement. Un corpus généré sous le socle CEN
n'exerce aucune des règles `BR-FR-*` sous lesquelles il serait ensuite validé.

Si votre corpus est déjà produit par ailleurs, omettez `--generate` : le contenu de
`--corpus` est envoyé tel quel.

### Un scénario que votre chaîne ne sait pas produire

Sortez en **non-zéro sans écrire de fichier**, avec un message qui **nomme le champ
manquant**. C'est le canal prévu, et il ne fait pas échouer le pipeline :

- le scénario est compté **« non produit par l'outil »**, avec votre message ;
- il monte au service avec le corpus, et l'écran de couverture le distingue de
  « non couvert ». Sans cela, les règles que ce cas aurait exercées vous seraient
  reprochées comme un trou de votre corpus, alors que le cas existe au catalogue et
  que seul votre générateur peut le combler ;
- il apparaît dans le rapport markdown, en `<skipped>` JUnit et en `FS-NOT-PRODUCED`
  SARIF (niveau `warning`) ;
- le run ne devient pas rouge pour autant : ce n'est pas une non-conformité.

```php
if (!$modele->supporte('BT-31')) {
    fwrite(STDERR, "champ BT-31 (SIRET vendeur) absent du modele de facturation");
    exit(4);
}
```

Deux choses restent des pannes, et arrêtent la génération :

- **écrire le fichier ET sortir en non-zéro** : une facture partielle validée comme
  entière vous imputerait une non-conformité que votre propre générateur a signalée.
  Le fichier est retiré ;
- **décliner tous les scénarios** : un corpus vide rendrait un diff sans delta, donc
  un pipeline vert qui ne mesure rien.

### Vérifier votre connecteur

```bash
facturseal-adapter-check -- php generate.php
```

Rejoue des payloads de référence **hors ligne** (ni jeton ni réseau) et vérifie les
invariants du contrat : fichier écrit au bon chemin, code de sortie cohérent avec la
production du fichier, tenue du budget de temps, absence de fuite d'environnement.
C'est la commande à passer avant de brancher `--generate` dans un pipeline, et celle
qui rend le connecteur reprenable par un autre éditeur que celui qui l'a écrit.

## Options

| Option | Rôle |
|--------|------|
| `--generate CMD` | Commande de génération de l'éditeur ; absente = corpus déjà peuplé |
| `--corpus DIR` | Répertoire du corpus (défaut : `build/factures`) |
| `--baseline FILE` | Baseline de référence versionnée (défaut : `facturseal-baseline.json`) |
| `--extension EXT` | Extension produite : `xml` ou `pdf` (défaut : `xml`) |
| `--cius LAYER` | Couche de juridiction du catalogue demandé : `core` ou `fr-ctc` (déf. `FACTURSEAL_CIUS`) |
| `--api-url URL` | URL du service (déf. `FACTURSEAL_URL`) |
| `--token TOKEN` | Jeton bearer (déf. `FACTURSEAL_TOKEN`) |
| `--report FILE` | Rapport markdown, lisible en revue de merge request |
| `--junit FILE` | JUnit XML : onglet « Tests » de GitLab et GitHub |
| `--sarif FILE` | SARIF 2.1.0 : onglet « Security / code scanning » de GitHub |
| `--update-baseline` | Promouvoir : (re)figer la référence au lieu de differ |
| `--waivers FILE` | Fichier de dérogations gouvernées (déf. `FS_WAIVER_STORE`) |
| `--tenant ID` | Tenant dont les dérogations s'appliquent (exigé avec `--waivers`) |
| `--allow-insecure` | Tolérer un service en `http` (le jeton circule alors en clair) |
| `--timeout N` | Budget en secondes par appel et par génération (défaut : 120) |

Les métadonnées de run (branche, commit, déclencheur) sont auto-détectées sur GitLab
CI, GitHub Actions et Azure DevOps ; `--branch`, `--commit` et `--trigger` les
forcent au besoin.

## Dérogations

Une régression peut être **acceptée** sans être effacée. Le fichier de dérogations est
versionné dans votre dépôt, par dessein : une dérogation est une décision, elle passe
donc par la revue de code comme le reste.

```json
{
  "waivers": {
    "mon-tenant": [
      {
        "rule_id": "BR-FR-01_BT-1",
        "justification": "Correctif livré en 2026-09, ticket FACT-4821",
        "created_at": "2026-08-11",
        "expires_at": "2026-09-30",
        "author": "equipe-facturation"
      }
    ]
  }
}
```

```bash
facturseal-ci --waivers .facturseal/waivers.json --tenant mon-tenant ...
```

Trois propriétés valent d'être connues :

- **la maille est l'assertion, pas la règle.** Sous la couche FR, une règle se décline
  en un assert par champ contrôlé : `BR-FR-01` ne déroge à **rien**, `BR-FR-01_BT-1`
  déroge à ce champ-là. Élargir tacitement à toute une famille dérogerait à des
  contrôles que personne n'a nommés ;
- **un constat dérogé reste nommé.** Il sort de la liste bloquante, il reste dans le
  rapport, dans le JUnit (`skipped`, avec sa justification) et dans le SARIF
  (`suppressions`). « Rien trouvé » et « trouvé puis accepté » ne se confondent pas ;
- **les dérogations mortes sont dénoncées.** Une dérogation active qui n'a couvert
  aucun constat, ou une dérogation échue, est listée en sortie : sans ça, elle
  élargirait le plafond sans que personne l'ait décidé.

Un fichier de dérogations demandé mais absent est une **panne d'outil** (exit 2), pas
« aucune dérogation ».

## GitLab CI

```yaml
facturseal:
  image: python:3.12-slim
  script:
    - pip install facturseal-ci
    - facturseal-ci --generate "php examples/php/generate.php"
                    --junit build/facturseal-junit.xml
                    --report build/facturseal.md
  artifacts:
    when: always
    paths: [build/facturseal.md]
    reports:
      junit: build/facturseal-junit.xml
```

## GitHub Actions

```yaml
- run: pip install facturseal-ci
- run: facturseal-ci --generate "php examples/php/generate.php" --sarif facturseal.sarif
  env:
    FACTURSEAL_URL: ${{ vars.FACTURSEAL_URL }}
    FACTURSEAL_TOKEN: ${{ secrets.FACTURSEAL_TOKEN }}
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with: { sarif_file: facturseal.sarif }
```

## Ce que ce paquet ne contient pas

Ni moteur de validation, ni catalogue de règles, ni matrice de référence, ni
scénarios : tout cela appartient au produit FacturSeal et est servi par le service.
La baseline elle-même est **opaque** au client, qui la transporte entre le service et
votre dépôt sans l'interpréter, à la seule exception du verdict par facture, lu pour
détecter les factures que le moteur n'a pas su valider.

## Licence

Apache 2.0 (voir `LICENSE` et `NOTICE`). « FacturSeal » est une marque de Tsharp : la
licence n'en concède pas l'usage.
