feat(quotes): import retour signé client + hash texte d'intégrité#3
Merged
Conversation
Workflow V0.2 acté dans `docs/roadmap-post-v0.1.md` §2 (Option B). À l'émission d'un devis, FAKT calcule maintenant le SHA-256 du texte normalisé du PDF officiel (via `pdf-extract` Rust + commande Tauri `compute_pdf_text_hash`) et le persiste sur `quotes.original_text_hash`. À l'import retour, un nouveau bouton « Importer signature client » sur les devis `sent` ouvre un modal : - Sélection du PDF retourné par le client (file picker) - Champ email/nom du signataire - Vérification du hash texte du PDF importé contre celui stocké - Si match → store_signed_pdf + signature event chaîné + activity event `quote_signed_by_client_imported` + transition sent → signed - Si mismatch → modal de confirmation forcée affichant les deux hashes tronqués (8…8) avec avertissement (annotation/altération). Force OK : l'audit event consigne la divergence. La chaîne d'audit reste cryptographiquement intègre : le calcul de `previousEventHash` passe par la commande Rust `compute_signature_event_self_hash` (pas de réimplémentation TS). Plan : docs/superpowers/plans/2026-05-03-import-signed-quote.md Validation : - 1045+ tests verts (sidecar original-text-hash, DB round-trip, UI bouton, Rust normalize_text + audit chain hash) - typecheck zéro warning sur 13 packages - lint Biome zéro warning sur 516 fichiers - cargo check OK (nouvelle dep `pdf-extract = "0.7"`) Note merge : conflit attendu avec PR #2 (clauses) sur : - `0005_*.sql` — renommer en `0006_quote_original_text_hash.sql` au rebase - `_journal.json` + `migrations-embedded.ts` — régénérer - `Quote` interface (shared) — merge naturel Signed-off-by: Tom Andrieu <andrieutom30@gmail.com>
31d4695 to
a67646e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contexte
C'est exactement le workflow que Tom décrivait : « je l'envoie au client, il signe, je l'importe de nouveau dans FAKT en tant que ‘client a signé ce PDF', et FAKT continue automatiquement la suite du process. »
Implémente la V0.2 actée dans
docs/roadmap-post-v0.1.md§2 (Option B import retour, écartée Option A portail SaaS pour rester offline-first).Plan superpowers complet :
docs/superpowers/plans/2026-05-03-import-signed-quote.mdWorkflow utilisateur
sent).quotes.original_text_hash). Idempotent — re-run sur les vieux devis sans hash.sent → signed, débloque « Créer une facture ».(8…8). Tom peut confirmer (annotation manuscrite légitime) ou annuler (altération suspecte). Force OK : l'audit consigne la divergence (docHashBefore=expected,docHashAfter=actual).Architecture
Côté Rust (logique cryptographique)
pdf/text_hash.rs— extraction texte viapdf-extract0.7 + normalisation déterministe (whitespace folded, line endings unifiés, BOM stripped, trim) + SHA-256commands/pdf_hash.rs— commande Tauricompute_pdf_text_hashcommands/audit_chain.rs— commande Tauricompute_signature_event_self_hash(réutilisecrypto::audit::SignatureEvent::compute_self_hash_hex— pas de réimplémentation TS du format de hash chaîné)Côté DB
0005_quote_original_text_hash.sql—ALTER TABLE quotes ADD COLUMN original_text_hash TEXTCôté sidecar
POST /api/quotes/:id/original-text-hash— endpoint dédié (le PATCH/quotes/:idest limité àstatus=draft, on a besoin de set sursent). Idempotent : re-écriture identique OK, valeur différente refusée (422). Refusé sursigned/invoiced(PDF figé).^[0-9a-f]{64}$Côté frontend
quote-text-hash.ts— helperpersistQuoteOriginalTextHash(best-effort, log sans bloquer)import-signed-quote.ts— orchestrateurverifyImportedPdfHash+commitImportSignedQuote(storeSignedPdf, append event chaîné via Rust, activity event, transition status)ImportSignedModal.tsx— modal 3 phases (form / verifying / mismatch / committing)useEffectdansDetail.tsx— couvre les 3 cas : « Marquer envoyé » manuel, « Créer et émettre » direct, vieux devis émis avant cette featureValidation
original-text-hash(set, idempotent, valeur différente refusée, devis signé refusé, hash mal formé)originalTextHashnormalize_text(whitespace, line endings, BOM, trim, idempotence, unicode, invalid PDF)compute_signature_event_self_hash(déterminisme, sensibilité aux champs)pdf-extract = "0.7.12", pure Rust, ~200ko compilé, pas de FFI)Test plan
bun install(nouvelle deppdf-extractcôté Cargo.toml seulement)bun run typecheck— zéro warning attendubun run lint— zéro warning attendubun run test— 1045+ testscd apps/desktop/src-tauri && cargo test text_hash— tests unitaires Rustoriginal_text_hashset en DBsignedissuedAt) → bouton désactivé avec tooltip explicatifCette PR est branchée depuis
mainet utilise le numéro0005pour la migration. Si la PR #2 (audit-trail + clauses) est mergée d'abord, il faudra rebase et :0005_quote_original_text_hash.sql→0006_quote_original_text_hash.sqlmeta/_journal.json(idx 6 au lieu de 5)bun packages/api-server/scripts/generate-migrations.tsQuoteinterface (shared) — les deux ajouts coexistent.Le code métier est indépendant : pas de conflit fonctionnel, juste numérotation.
Hors scope (intentionnel)
pdf-extractretourne string vide → mismatch garanti → l'utilisateur force, divergence consignée. Documenté dans le plan (R5).