Metadata-Version: 2.4
Name: uemoa-qrcode-sdk
Version: 0.1.0
Summary: SDK Python pour les QR codes de paiement instantane UEMOA (EMVCo / BCEAO)
Project-URL: Homepage, https://github.com/Fabrice62-pr/Sdk_Python_QrCode
Project-URL: Repository, https://github.com/Fabrice62-pr/Sdk_Python_QrCode
Project-URL: Issues, https://github.com/Fabrice62-pr/Sdk_Python_QrCode/issues
Author-email: fabrice <fabricetolo57@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: bceao,emvco,paiement,qrcode,uemoa,xof
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: image
Requires-Dist: pillow>=10.0; extra == 'image'
Requires-Dist: qrcode>=7.4; extra == 'image'
Description-Content-Type: text/markdown

# uemoa-qrcode-sdk

SDK Python pour les QR codes de paiement instantané UEMOA, conformes aux
spécifications EMVCo et BCEAO.

Portage du core Java `uemoa-qrcode-sdk-core`. Pour une même entrée, les deux SDK
produisent **la même chaîne EMVCo, caractère pour caractère** — c'est vérifié à
chaque exécution de la suite de tests contre un corpus de vecteurs exportés
depuis le Java.

Le cœur du SDK (TLV, CRC, générateurs, parser) n'a **aucune dépendance**. Seul
le rendu d'image en réclame, derrière un extra optionnel.

## Installation

```bash
pip install uemoa-qrcode-sdk           # cœur seul, zéro dépendance
pip install uemoa-qrcode-sdk[image]    # + rendu PNG (qrcode, Pillow)
```

Python 3.10 ou plus récent.

## Démarrage

```python
from uemoa_qrcode import UemoaQRService, QRPaymentData, MerchantInfo

service = UemoaQRService()

qr = service.generate_qr_data(
    QRPaymentData(
        merchant_info=MerchantInfo(
            alias="test-123",
            name="TEST SHOP",
            city="Abidjan",
            country_code="CI",
        ),
    )
)
# 00020101021136280012int.bceao.pi0108test-1235204000053039525802CI5909TEST SHOP6007Abidjan63049EF3
```

### QR statique avec montant

```python
from decimal import Decimal

qr = service.generate_static_qr(
    QRPaymentData(
        merchant_info=MerchantInfo(
            alias="test-456",
            name="RESTO TEST",
            city="Yamoussoukro",
            country_code="CI",
        ),
        amount=Decimal("5000"),
    )
)
```

Le montant accepte un `Decimal`, un `int` ou une chaîne. **Les `float` sont
refusés** : un binaire flottant ne représente pas exactement un montant.

### QR dynamique

```python
from uemoa_qrcode import MerchantChannel

qr = service.generate_dynamic_qr(
    QRPaymentData(
        dynamic_url="pi.psp-ci.com/t/9f3a2b",
        amount=15_000,
        merchant_channel=MerchantChannel.DYNAMIC_ECOMMERCE_WEB,
    )
)
```

Le compte est désigné soit par une URL de PSP, soit par un alias marchand. En
l'absence de canal explicite, le canal 500 est injecté.

### QR P2P

```python
qr = service.generate_p2p_qr(
    QRPaymentData(
        merchant_info=MerchantInfo(
            alias="p2p-aicha",
            name="AICHA DIALLO",
            city="Bamako",
            country_code="ML",
        ),
    )
)
```

Le nom est facultatif : absent, il est masqué par `XXX`. Le canal 731 est
imposé.

## Lecture

```python
data = service.parse_qr_code(qr)

data.type            # QRType.STATIC
data.amount          # Decimal("5000")
data.merchant_info   # MerchantInfo(...)
data.transaction_id  # "TX-000123"
```

Le CRC est vérifié à la lecture ; un QR code altéré lève `QRParsingError`.

Pour un simple prédicat, ou pour un diagnostic sérialisable :

```python
service.validate_qr_code(qr)      # True / False, ne lève jamais
service.get_qr_code_details(qr)   # dict prêt pour une réponse JSON
```

## Image

```python
service.generate_qr_image_bytes(data)          # bytes PNG
service.generate_qr_image(data)                # chaîne Base64
service.save_qr_image(data, "qr.png")          # écrit sur disque
```

Les trois acceptent aussi bien un `QRPaymentData` qu'une chaîne EMVCo déjà
construite.

## Configuration

```python
from uemoa_qrcode import UemoaQRConfig, UemoaQRService

service = UemoaQRService(
    UemoaQRConfig(
        qr_image_size=512,
        qr_image_margin=4,   # la norme ISO/IEC 18004 exige 4 ; le core Java utilise 1
        validate_crc=True,
    )
)
```

`UemoaQRConfig` est immuable, et le service est sans état : une instance peut
être créée au démarrage et partagée entre threads.

## Erreurs

| Exception | Cause |
|---|---|
| `QRValidationError` | Donnée d'entrée non conforme aux règles EMVCo / BCEAO |
| `QRParsingError` | QR code illisible, CRC absent ou invalide |
| `QRImageGenerationError` | Échec du rendu, ou extra `[image]` non installé |

Les trois dérivent de `UemoaQRError`, qui permet de tout filtrer d'un seul
`except`.

## Conformité au core Java

Trois invariants garantissent l'interopérabilité entre les portages Java, Python
et JavaScript :

1. **CRC16-CCITT** (polynôme `0x1021`, init `0xFFFF`) calculé sur les octets
   **ISO-8859-1** — jamais UTF-8, jamais l'encodage par défaut de la plateforme.
2. **Ordre canonique des tags**, croissant, y compris pour les sous-champs du
   tag 62.
3. **Longueurs TLV en octets**, pas en caractères.

Le fichier `tests/vectors/golden.json` contient les vecteurs de référence
exportés depuis le core Java. `test_generation_matches_java` les rejoue tous à
chaque exécution : toute divergence d'un seul caractère fait échouer la suite.

### Divergences volontaires avec le core Java

Le portage corrige des comportements du Java qui produisaient des QR codes
incohérents. Ces écarts sont documentés et testés.

| Situation | Core Java | Ce SDK |
|---|---|---|
| Montant à décimales en XOF | Émet `54075000.50` | `QRValidationError` |
| Caractère hors Latin-1 | CRC calculé sur `?`, incohérent avec la charge utile | `QRValidationError` |
| CRC absent ou hors position finale | Vérification silencieusement ignorée | `QRParsingError` |
| Longueur TLV d'un caractère multi-octets | Comptée en caractères, donc fausse | Comptée en octets |
| Générateurs | Singletons Spring à état mutable, non thread-safe | Sans état, réentrants |
| `generate_static_qr` et variantes | Modifient l'objet de l'appelant | Travaillent sur une copie |

Une asymétrie du Java est en revanche **conservée** : une URL dynamique sans le
préfixe `pi.` est bien générée, mais le parser ne la restitue pas dans
`dynamic_url`. Le vecteur `dynamic-url-without-pi-prefix` documente ce
comportement.

## Développement

```bash
python -m venv .venv
.venv/Scripts/pip install -e .[image,dev]

.venv/Scripts/python -m pytest      # 322 tests
.venv/Scripts/python -m ruff check src tests
.venv/Scripts/python -m mypy
```

Le corpus de référence ne s'édite pas à la main : il est régénéré depuis le
core Java.

```bash
mvn test -Dtest=GoldenVectorExporter -Dgolden.output=<chemin>/tests/vectors/golden.json
```

## Licence

MIT.
