@mostajs/recette 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +22 -0
- package/llms.txt +12 -0
- package/package.json +12 -4
- package/src/archive.js +141 -0
- package/src/recette.js +17 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
4
|
|
|
5
|
+
## 0.5.0 — 2026-10-29
|
|
6
|
+
|
|
7
|
+
### Ajouté
|
|
8
|
+
|
|
9
|
+
- **`enveloppeRapport(campagneId, { declaration?, note?, pieces? })`** — l'enveloppe
|
|
10
|
+
`mostajs-recette-rapport/1` sans signature, à tout moment de la campagne. `rapportEtape` la signe :
|
|
11
|
+
c'est la même. **T-REC-19**.
|
|
12
|
+
- **`@mostajs/recette/archive`** (chemin à part, seul à importer `@mostajs/archive-box`, pair
|
|
13
|
+
facultatif) : `vueRapport`, `formulaireHtml`, `archiveRapport` — le formulaire rempli en HTML
|
|
14
|
+
autonome, en PDF si l'application injecte un convertisseur (sinon l'absence est écrite), le JSON,
|
|
15
|
+
l'enveloppe, et chaque pièce rangée sous la ligne à laquelle elle répond, liée depuis le
|
|
16
|
+
formulaire. Né dans QaTrax le 29/10/2026 (relire un questionnaire répondu en « ok »), demandé
|
|
17
|
+
aussi par ValiTrax : deux consommateurs, donc ici. **T-REC-20**.
|
|
18
|
+
|
|
5
19
|
## 0.4.0 — 2026-10-23
|
|
6
20
|
|
|
7
21
|
### Corrigé
|
package/README.md
CHANGED
|
@@ -126,6 +126,28 @@ const { feuillet, cases } = await recette.feuillets.composer({ campagneId: c.id,
|
|
|
126
126
|
// … consigner · rendre · figer · exporter · recevoir le certificat · autoriser l'exploitation
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
## L'archive d'un rapport — `@mostajs/recette/archive` (0.5.0)
|
|
130
|
+
|
|
131
|
+
Un questionnaire répondu en recette n'est fait que de « ok » avec une remarque : rien à corriger, mais tout à relire.
|
|
132
|
+
L'archive rend le formulaire rempli et ses pièces, **chacune sous la ligne à laquelle elle répond**. QaTrax l'exporte
|
|
133
|
+
d'un rapport reçu, ValiTrax d'une campagne : une seule fabrication, la même archive.
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { vueRapport, archiveRapport } from '@mostajs/recette/archive'; // chemin à part : lui seul importe archive-box
|
|
137
|
+
|
|
138
|
+
const enveloppe = await recette.enveloppeRapport(campagneId, { pieces }); // non signée, à tout moment
|
|
139
|
+
const vue = vueRapport(enveloppe, { id, idDe: (sha) => maPiece(sha)?.id });
|
|
140
|
+
const { buffer, nom } = await archiveRapport(vue, {
|
|
141
|
+
contenu: async (sha) => contenuDeMaPiece(sha), // Buffer ou null (la pièce manquante est nommée dans l'archive)
|
|
142
|
+
pdf: async (html) => monConvertisseur(html), // facultatif : sinon « PDF absent » est écrit, pas tu
|
|
143
|
+
enveloppe, sourceApp: 'valitrax',
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Contenu du zip : `LISEZMOI.txt`, `formulaire.html` (autonome, liens relatifs), `formulaire.pdf` (si un convertisseur
|
|
148
|
+
est injecté), `rapport.json`, `enveloppe.json`, `pieces/<ligne>/<nom>`, et le manifeste `@mostajs/archive-box`
|
|
149
|
+
(SHA-256 par fichier). Aucun appel réseau : le PDF vient de l'application.
|
|
150
|
+
|
|
129
151
|
## Signer et sceller ne sont pas la même chose
|
|
130
152
|
|
|
131
153
|
| | avec quelle clé | ce que ça prouve |
|
package/llms.txt
CHANGED
|
@@ -8,6 +8,7 @@ QUESTION À LAQUELLE IL RÉPOND
|
|
|
8
8
|
NIVEAU N3 · métier · forme : moteur · dépend de : aucun autre N3
|
|
9
9
|
@mostajs/qa-engine n'est ni importé ni appelé : ce qui circule est un FICHIER SIGNÉ.
|
|
10
10
|
@mostajs/crypto-box n'est importé que par `@mostajs/recette/sceau`, chemin à part.
|
|
11
|
+
@mostajs/archive-box n'est importé que par `@mostajs/recette/archive`, chemin à part (0.5.0).
|
|
11
12
|
|
|
12
13
|
ENTITÉS (7) Campagne · Point · Feuillet · Releve · Anomalie · Dossier · Certificat
|
|
13
14
|
VERBES ouvrir · synchroniser · importer · enrichir · retenir · composer · remettre · consigner ·
|
|
@@ -89,3 +90,14 @@ USAGE createRecette({ repositories, ports: { sceau, trameSource, audit },
|
|
|
89
90
|
creerSceau({ signature, destinataire, certificateur }) ← @mostajs/recette/sceau
|
|
90
91
|
engendrerClefs() ← une fois, à la mise en service ; les clés vont au coffre
|
|
91
92
|
ESSAIS npm test — 13 essais, tous sur ce que le moteur REFUSE
|
|
93
|
+
|
|
94
|
+
ARCHIVE D'UN RAPPORT (0.5.0) — @mostajs/recette/archive
|
|
95
|
+
recette.enveloppeRapport(campagneId, { declaration?, note?, pieces? }) → mostajs-recette-rapport/1, NON signée,
|
|
96
|
+
à tout moment (rapportEtape la signe : même enveloppe).
|
|
97
|
+
vueRapport(enveloppe, { id?, recuLe?, piecesAbsentes?, idDe?(sha) }) → lignes { ref, exigence, issue, question,
|
|
98
|
+
attendu, reponse, le, pieces[{ id, nom, type, sha256, chemin }] }, anomalies, sansReponse.
|
|
99
|
+
formulaireHtml(vue, { emetteur? }) → page autonome, liens relatifs vers pieces/.
|
|
100
|
+
archiveRapport(vue, { contenu(sha) → Buffer|null, pdf?(html) → { ok, octets }, enveloppe?, sourceApp? })
|
|
101
|
+
→ { buffer (zip archive-box), nom, sha256, pdf } : LISEZMOI, formulaire.html, formulaire.pdf (si pdf injecté,
|
|
102
|
+
sinon « PDF absent » dit), rapport.json, enveloppe.json, pieces/<ligne>/<nom> ; manifeste SHA-256.
|
|
103
|
+
Consommateurs : QaTrax (rapports reçus, PDF par mdseal), ValiTrax (export de la campagne).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/recette",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Validation & recette applicative : trame `mostajs-devtest/1` reçue signée du certificateur, points enrichis POUR LE CLIENT (l'import n'écrase jamais l'enrichissement), feuillets composés à l'image du manifeste PDF (ok/ko/er), anomalies déclarées puis qualifiées, dossier figé et EXPORTÉ SIGNÉ — aucun appel réseau —, certificat reçu et vérifié, feu d'exploitation DÉRIVÉ du certificat. Powers ValiTrax (valitrax.mostajs.dev).",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
".": "./src/index.js",
|
|
11
11
|
"./schemas": "./src/schemas.js",
|
|
12
12
|
"./register": "./src/register.js",
|
|
13
|
-
"./sceau": "./src/sceau.js"
|
|
13
|
+
"./sceau": "./src/sceau.js",
|
|
14
|
+
"./archive": "./src/archive.js"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"src",
|
|
@@ -71,12 +72,15 @@
|
|
|
71
72
|
"lire une feuille papier photographiée": "@mostajs/ocr",
|
|
72
73
|
"implémenter la cryptographie": "@mostajs/crypto-box — Ed25519 pour signer, enveloppe X25519 pour sceller",
|
|
73
74
|
"déposer le dossier chez le certificateur": "personne : l'export est un FICHIER que le client ou notre service transporte",
|
|
74
|
-
"produire les formulaires du client": "ce module — QaTrax n'envoie que la TRAME (mostajs-devtest/1) ; la formulation appartient à la recette"
|
|
75
|
+
"produire les formulaires du client": "ce module — QaTrax n'envoie que la TRAME (mostajs-devtest/1) ; la formulation appartient à la recette",
|
|
76
|
+
"zipper et empreinter une archive": "@mostajs/archive-box — importé par le seul chemin `@mostajs/recette/archive`",
|
|
77
|
+
"convertir le formulaire en PDF": "un convertisseur INJECTÉ par l’application (mdseal chez QaTrax) — aucun appel réseau ici"
|
|
75
78
|
}
|
|
76
79
|
},
|
|
77
80
|
"peerDependencies": {
|
|
78
81
|
"@mostajs/mjs-unit": ">=0.4.0",
|
|
79
|
-
"@mostajs/crypto-box": ">=0.3.0"
|
|
82
|
+
"@mostajs/crypto-box": ">=0.3.0",
|
|
83
|
+
"@mostajs/archive-box": ">=0.2.0"
|
|
80
84
|
},
|
|
81
85
|
"peerDependenciesMeta": {
|
|
82
86
|
"@mostajs/mjs-unit": {
|
|
@@ -84,9 +88,13 @@
|
|
|
84
88
|
},
|
|
85
89
|
"@mostajs/crypto-box": {
|
|
86
90
|
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@mostajs/archive-box": {
|
|
93
|
+
"optional": true
|
|
87
94
|
}
|
|
88
95
|
},
|
|
89
96
|
"devDependencies": {
|
|
97
|
+
"@mostajs/archive-box": "file:../../mosta-content-stack/mosta-archive-box",
|
|
90
98
|
"@mostajs/crypto-box": "file:../../mosta-kernel-stack/mosta-crypto-box",
|
|
91
99
|
"@mostajs/mjs-unit": "^0.6.0"
|
|
92
100
|
}
|
package/src/archive.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// @mostajs/recette/archive — L'ARCHIVE D'UN RAPPORT DE RECETTE : le formulaire rempli, ses pièces sous leur ligne.
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
3
|
+
//
|
|
4
|
+
// 29/10/2026 (demande du fondateur). Né dans QaTrax pour relire un questionnaire répondu dans ValiTrax — des relevés
|
|
5
|
+
// « ok » avec une remarque, qui n'ouvrent aucun défaut — puis demandé aussi depuis ValiTrax : deux consommateurs, donc
|
|
6
|
+
// le module. Les deux partent de la MÊME enveloppe `mostajs-recette-rapport/1` : `enveloppeRapport` (ValiTrax, à tout
|
|
7
|
+
// moment) ou le rapport d'étape reçu (QaTrax).
|
|
8
|
+
//
|
|
9
|
+
// - `vueRapport` : le rapport relu ligne par ligne — question, attendu, réponse, heure, exigence, et SES pièces
|
|
10
|
+
// (identifiant chez l'appelant, chemin dans l'archive) ;
|
|
11
|
+
// - `formulaireHtml` : le formulaire rempli, page autonome, qui renvoie à chaque pièce par un lien relatif ;
|
|
12
|
+
// - `archiveRapport` : un zip `@mostajs/archive-box` (manifeste, SHA-256 par fichier) — LISEZMOI, formulaire HTML,
|
|
13
|
+
// formulaire PDF quand l'appelant fournit un convertisseur, JSON, enveloppe, `pieces/<ligne>/<nom>`.
|
|
14
|
+
//
|
|
15
|
+
// Chemin à part, comme `./sceau` : lui seul importe `@mostajs/archive-box`. Aucun appel réseau : le PDF vient d'une
|
|
16
|
+
// fonction INJECTÉE (mdseal chez QaTrax) ; sans elle, l'archive le dit au lieu de se taire.
|
|
17
|
+
import { buildArchive } from '@mostajs/archive-box';
|
|
18
|
+
|
|
19
|
+
/** Les déclarations, dites au lecteur. `null` : un export à date, sans déclaration. */
|
|
20
|
+
export const LIBELLES_DECLARATION = Object.freeze({ partiel: 'partiellement fini', urgent: 'l’urgent est fini', fini: 'fini' });
|
|
21
|
+
|
|
22
|
+
const esc = (s = '') => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
23
|
+
/** Un segment de chemin sûr dans un zip : ni séparateur, ni caractère que Windows refuse, ni « . » ou « .. ». */
|
|
24
|
+
const segment = (s, repli) => {
|
|
25
|
+
const x = String(s || '').replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_').replace(/^\.+$/, '').trim().slice(0, 120);
|
|
26
|
+
return x || repli;
|
|
27
|
+
};
|
|
28
|
+
const lienRelatif = (chemin) => chemin.split('/').map(encodeURIComponent).join('/');
|
|
29
|
+
const parRef = (a, b) => String(a.ref).localeCompare(String(b.ref), 'fr', { numeric: true });
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Le rapport relu ligne par ligne.
|
|
33
|
+
* @param {object} enveloppe `mostajs-recette-rapport/1` (signée ou non : la signature n'est pas lue ici)
|
|
34
|
+
* @param {object} [o]
|
|
35
|
+
* @param {string} [o.id] l'identifiant du rapport chez l'appelant
|
|
36
|
+
* @param {string} [o.recuLe] quand il a été reçu (QaTrax) ou exporté (ValiTrax) ; défaut : `enveloppe.emisLe`
|
|
37
|
+
* @param {string[]} [o.piecesAbsentes] pièces annoncées dont le contenu n'est pas arrivé
|
|
38
|
+
* @param {(sha256:string) => string|null} [o.idDe] l'identifiant d'une pièce chez l'appelant (pour la télécharger)
|
|
39
|
+
*/
|
|
40
|
+
export function vueRapport(enveloppe, { id = null, recuLe = null, piecesAbsentes = [], idDe = () => null } = {}) {
|
|
41
|
+
const e = enveloppe || {};
|
|
42
|
+
const pris = new Set();
|
|
43
|
+
const ranger = (dossier, p) => {
|
|
44
|
+
const nom = segment(p.nom, 'piece');
|
|
45
|
+
let chemin = `pieces/${dossier}/${nom}`;
|
|
46
|
+
for (let i = 2; pris.has(chemin); i++) chemin = `pieces/${dossier}/${nom.replace(/(\.[^.]*)?$/, ` (${i})$1`)}`;
|
|
47
|
+
pris.add(chemin);
|
|
48
|
+
return { id: idDe(p.sha256) || null, nom: p.nom, type: p.type || 'application/octet-stream', taille: p.taille || 0, sha256: p.sha256, chemin };
|
|
49
|
+
};
|
|
50
|
+
const piecesDe = (cible, dossier) => (e.pieces || []).filter((p) => p.cible === cible).map((p) => ranger(dossier, p));
|
|
51
|
+
const lignes = [...(e.releves || [])].sort(parRef).map((x) => ({
|
|
52
|
+
ref: x.ref, exigence: x.specRef || null, issue: x.issue, question: x.action || '', attendu: x.attendu || '',
|
|
53
|
+
reponse: x.texte || '', le: x.at || null, pieces: piecesDe(x.ref, segment(x.ref, 'ligne')),
|
|
54
|
+
}));
|
|
55
|
+
const anomalies = (e.anomalies || []).map((a, i) => ({
|
|
56
|
+
titre: a.titre, description: a.description || '', gravite: a.graviteQualifiee || a.graviteProposee || null, etat: a.state || null,
|
|
57
|
+
declareePar: a.declareePar || null, pieces: piecesDe(`anomalie:${i}`, `anomalie-${i + 1}`),
|
|
58
|
+
}));
|
|
59
|
+
const retenus = Number(e.compteurs?.retenus) || 0;
|
|
60
|
+
return {
|
|
61
|
+
id, recuLe: recuLe || e.emisLe || null, projet: e.campagne?.projet || '', declaration: e.declaration || null,
|
|
62
|
+
declarationLibelle: LIBELLES_DECLARATION[e.declaration] || (e.declaration ? String(e.declaration) : 'relevé à date'),
|
|
63
|
+
note: e.note || '', campagne: e.campagne || {}, feuillets: e.feuillets || [], compteurs: e.compteurs || {},
|
|
64
|
+
sansReponse: Math.max(0, retenus - lignes.length), lignes, anomalies, piecesAbsentes,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Le formulaire rempli, en une page HTML autonome. `emetteur` : le nom affiché en pied. */
|
|
69
|
+
export function formulaireHtml(v, { emetteur = '' } = {}) {
|
|
70
|
+
const ISSUE = { ok: ['ok', '#15803d'], ko: ['ko', '#b45309'], er: ['er', '#b91c1c'] };
|
|
71
|
+
const pastille = (i) => { const [t, c] = ISSUE[i] || [i || '—', '#64748b']; return `<span style="color:#fff;background:${c};border-radius:4px;padding:1px 6px;font-size:12px">${esc(t)}</span>`; };
|
|
72
|
+
const liens = (ps) => ps.map((p) => `<a href="${esc(lienRelatif(p.chemin))}">${esc(p.nom)}</a>`).join('<br>');
|
|
73
|
+
const heure = (s) => (s ? String(s).slice(0, 16).replace('T', ' ') : '');
|
|
74
|
+
const lignes = v.lignes.map((l) => `<tr><td><b>${esc(l.ref)}</b>${l.exigence ? `<br><small>${esc(l.exigence)}</small>` : ''}</td>
|
|
75
|
+
<td>${esc(l.question)}${l.attendu ? `<br><small>Attendu : ${esc(l.attendu)}</small>` : ''}</td>
|
|
76
|
+
<td style="white-space:pre-wrap">${esc(l.reponse) || '<i>—</i>'}</td><td>${pastille(l.issue)}<br><small>${esc(heure(l.le))}</small></td><td>${liens(l.pieces)}</td></tr>`).join('\n');
|
|
77
|
+
const anos = v.anomalies.map((a) => `<tr><td>${esc(a.titre)}<br><small>${esc(a.description)}</small></td><td>${esc(a.gravite || '')}</td><td>${liens(a.pieces)}</td></tr>`).join('\n');
|
|
78
|
+
return `<!doctype html>
|
|
79
|
+
<html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
80
|
+
<title>${esc(v.campagne.no || 'Rapport')} — ${esc(v.projet)}</title>
|
|
81
|
+
<style>body{font-family:system-ui,sans-serif;color:#0f172a;margin:24px;line-height:1.4}table{border-collapse:collapse;width:100%;margin:12px 0}
|
|
82
|
+
th,td{border:1px solid #cbd5e1;padding:6px 8px;vertical-align:top;text-align:left}th{background:#f1f5f9}small{color:#64748b}h1{font-size:20px;margin:0 0 4px}
|
|
83
|
+
.meta{color:#475569;font-size:14px}@media print{a{color:inherit}}</style></head><body>
|
|
84
|
+
<h1>${esc(v.campagne.no || '')} · ${esc(v.projet)}</h1>
|
|
85
|
+
<p class="meta">${esc(v.declarationLibelle)} · ${esc(heure(v.recuLe))} UTC · ${esc(v.campagne.releaseRef || '')}${v.feuillets.length ? ` · ${v.feuillets.map((f) => `${esc(f.role)} : ${esc(f.destinataire || '—')}`).join(' · ')}` : ''}</p>
|
|
86
|
+
${v.note ? `<p style="white-space:pre-wrap"><b>Note :</b> ${esc(v.note)}</p>` : ''}
|
|
87
|
+
<p class="meta">${v.lignes.length} ligne(s) relevée(s)${v.sansReponse ? ` · ${v.sansReponse} ligne(s) retenue(s) sans réponse` : ''}${v.piecesAbsentes.length ? ` · pièce(s) non reçue(s) : ${v.piecesAbsentes.map(esc).join(', ')}` : ''}</p>
|
|
88
|
+
<table><thead><tr><th>Réf.</th><th>Question / ce qui était à faire</th><th>Réponse / remarque</th><th>Relevé</th><th>Pièces</th></tr></thead><tbody>
|
|
89
|
+
${lignes || '<tr><td colspan="5"><i>Aucune ligne relevée.</i></td></tr>'}
|
|
90
|
+
</tbody></table>
|
|
91
|
+
${anos ? `<h2 style="font-size:16px">Anomalies</h2><table><thead><tr><th>Titre</th><th>Gravité</th><th>Pièces</th></tr></thead><tbody>${anos}</tbody></table>` : ''}
|
|
92
|
+
<p class="meta">${v.id ? `Rapport ${esc(v.id)}` : ''}${v.id && emetteur ? ' · ' : ''}${esc(emetteur)}</p>
|
|
93
|
+
</body></html>
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* L'archive d'un rapport.
|
|
99
|
+
* @param {object} v `vueRapport(...)`
|
|
100
|
+
* @param {object} o
|
|
101
|
+
* @param {(sha256:string) => Promise<Buffer|null>} o.contenu le contenu d'une pièce chez l'appelant
|
|
102
|
+
* @param {(html:string) => Promise<{ok:boolean, octets?:Buffer, motif?:string}>} [o.pdf] un convertisseur, facultatif
|
|
103
|
+
* @param {string} [o.pdfAbsent] ce que dit l'archive quand aucun convertisseur n'est fourni
|
|
104
|
+
* @param {object} [o.enveloppe] l'enveloppe, versée telle quelle (`enveloppe.json`)
|
|
105
|
+
* @param {string} [o.sourceApp] l'application qui exporte (`qatrax`, `valitrax`)
|
|
106
|
+
* @returns {Promise<{buffer:Buffer, nom:string, sha256:string, pdf:boolean}>}
|
|
107
|
+
*/
|
|
108
|
+
export async function archiveRapport(v, { contenu, pdf = null, pdfAbsent = 'aucun convertisseur PDF configuré', enveloppe = null, emetteur = '', creePar = '', sourceApp = '' } = {}) {
|
|
109
|
+
const html = formulaireHtml(v, { emetteur });
|
|
110
|
+
const files = [{ path: 'formulaire.html', content: html }, { path: 'rapport.json', content: `${JSON.stringify(v, null, 2)}\n` }];
|
|
111
|
+
if (enveloppe) files.push({ path: 'enveloppe.json', content: `${JSON.stringify(enveloppe, null, 2)}\n` });
|
|
112
|
+
const manquantes = [];
|
|
113
|
+
for (const p of [...v.lignes, ...v.anomalies].flatMap((x) => x.pieces)) {
|
|
114
|
+
const b = await contenu(p.sha256);
|
|
115
|
+
if (b) files.push({ path: p.chemin, content: b }); else manquantes.push(p.chemin);
|
|
116
|
+
}
|
|
117
|
+
let motifPdf = pdfAbsent;
|
|
118
|
+
let avecPdf = false;
|
|
119
|
+
if (pdf) {
|
|
120
|
+
try {
|
|
121
|
+
const r = await pdf(html);
|
|
122
|
+
if (r?.ok && r.octets?.length) { files.push({ path: 'formulaire.pdf', content: r.octets }); avecPdf = true; } else motifPdf = r?.motif || 'le convertisseur n’a rien rendu';
|
|
123
|
+
} catch (e) { motifPdf = e?.message || String(e); }
|
|
124
|
+
}
|
|
125
|
+
const lisezmoi = [
|
|
126
|
+
`${v.campagne.no || 'Rapport'} — ${v.projet} — ${v.declarationLibelle}`, '',
|
|
127
|
+
'formulaire.html le formulaire rempli : chaque ligne, sa réponse, ses pièces (liens relatifs : dépliez l’archive avant d’ouvrir)',
|
|
128
|
+
avecPdf ? 'formulaire.pdf le même formulaire en PDF' : `formulaire.pdf PDF absent : ${motifPdf}`,
|
|
129
|
+
'rapport.json les mêmes données, pour un programme',
|
|
130
|
+
...(enveloppe ? ['enveloppe.json le rapport tel que la recette l’a relevé (mostajs-recette-rapport/1)'] : []),
|
|
131
|
+
'pieces/<ligne>/ chaque pièce jointe, rangée sous la ligne à laquelle elle répond',
|
|
132
|
+
'manifest.json l’empreinte SHA-256 de chaque fichier', '',
|
|
133
|
+
...(manquantes.length ? [`Pièce(s) annoncée(s) mais non reçue(s) : ${manquantes.join(', ')}`, ''] : []),
|
|
134
|
+
].join('\n');
|
|
135
|
+
files.unshift({ path: 'LISEZMOI.txt', content: `${lisezmoi}\n` });
|
|
136
|
+
const nom = `${segment(v.campagne.no, 'rapport')}-${segment(v.projet, 'projet')}`.replace(/\s+/g, '_');
|
|
137
|
+
const a = await buildArchive({ files, name: nom, description: `Rapport de recette ${v.campagne.no || ''} (${v.declarationLibelle})`, kind: 'recette-rapport',
|
|
138
|
+
createdBy: creePar || undefined, sourceApp: sourceApp || undefined,
|
|
139
|
+
extra: { projet: v.projet, campagne: v.campagne.no || null, rapport: v.id, pdf: avecPdf ? 'joint' : 'absent', ...(avecPdf ? {} : { pdfMotif: motifPdf }), piecesManquantes: manquantes } });
|
|
140
|
+
return { buffer: a.buffer, nom: `${nom}.zip`, sha256: a.sha256, pdf: avecPdf };
|
|
141
|
+
}
|
package/src/recette.js
CHANGED
|
@@ -584,12 +584,13 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
584
584
|
* Leur CONTENU voyage à côté de l'enveloppe ; la signature couvre leurs empreintes : une pièce échangée en chemin
|
|
585
585
|
* ne correspond plus.
|
|
586
586
|
*/
|
|
587
|
-
|
|
587
|
+
/**
|
|
588
|
+
* L'ENVELOPPE D'UN RAPPORT (0.5.0) — ce que la campagne a relevé, sans signer ni envoyer : le contenu même du rapport
|
|
589
|
+
* d'étape. `rapportEtape` la signe pour QaTrax ; `@mostajs/recette/archive` en fait l'archive que le client exporte
|
|
590
|
+
* (formulaire rempli, pièces sous leur ligne). Sans `declaration`, c'est un relevé à date, à tout moment de la campagne.
|
|
591
|
+
*/
|
|
592
|
+
async function enveloppeRapport(campagneId, { declaration = null, note = '', pieces = [] } = {}) {
|
|
588
593
|
const c = await getCampagne(campagneId);
|
|
589
|
-
if (!DECLARATIONS.includes(declaration)) throw new Error(`déclaration inconnue : ${declaration} (attendu : ${DECLARATIONS.join(' ou ')})`);
|
|
590
|
-
if (!['verification', 'recette', 'validation'].includes(c.state)) throw new Error(`un rapport d'étape s'envoie pendant la recette (la campagne est « ${c.state} »)`);
|
|
591
|
-
const sceau = ports.sceau;
|
|
592
|
-
if (!sceau?.signer) throw new Error("aucun sceau configuré : un rapport non signé ne prouve rien, il est refusé");
|
|
593
594
|
const fs = await feuillets.of(campagneId);
|
|
594
595
|
const rs = derniers(await releves.of(campagneId));
|
|
595
596
|
const as = await anomalies.of(campagneId);
|
|
@@ -612,6 +613,16 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
612
613
|
pieces: (pieces || []).map((x) => ({ cible: String(x.cible || ''), nom: String(x.nom || ''), type: String(x.type || ''), taille: Number(x.taille) || 0, sha256: String(x.sha256 || '') })),
|
|
613
614
|
};
|
|
614
615
|
for (const x of enveloppe.pieces) if (!/^[0-9a-f]{64}$/.test(x.sha256)) throw new Error(`pièce « ${x.nom} » : empreinte SHA-256 absente ou invalide`);
|
|
616
|
+
return enveloppe;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function rapportEtape(campagneId, { declaration, by = null, sceller = false, note = '', pieces = [] } = {}) {
|
|
620
|
+
const c = await getCampagne(campagneId);
|
|
621
|
+
if (!DECLARATIONS.includes(declaration)) throw new Error(`déclaration inconnue : ${declaration} (attendu : ${DECLARATIONS.join(' ou ')})`);
|
|
622
|
+
if (!['verification', 'recette', 'validation'].includes(c.state)) throw new Error(`un rapport d'étape s'envoie pendant la recette (la campagne est « ${c.state} »)`);
|
|
623
|
+
const sceau = ports.sceau;
|
|
624
|
+
if (!sceau?.signer) throw new Error("aucun sceau configuré : un rapport non signé ne prouve rien, il est refusé");
|
|
625
|
+
const enveloppe = await enveloppeRapport(campagneId, { declaration, note, pieces });
|
|
615
626
|
const signature = await sceau.signer(JSON.stringify(enveloppe));
|
|
616
627
|
let corps = { enveloppe, signature };
|
|
617
628
|
if (sceller) {
|
|
@@ -622,5 +633,5 @@ export function createRecette({ repositories, ports = {}, clock = () => new Date
|
|
|
622
633
|
return corps;
|
|
623
634
|
}
|
|
624
635
|
|
|
625
|
-
return { campagnes, trame, points, feuillets, releves, anomalies, dossiers, certificats, autoriserExploitation, rapportEtape, etat, PARCOURS };
|
|
636
|
+
return { campagnes, trame, points, feuillets, releves, anomalies, dossiers, certificats, autoriserExploitation, rapportEtape, enveloppeRapport, etat, PARCOURS };
|
|
626
637
|
}
|