From a11ec634ea8366fad4f71102afe86d78764be8c5 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Fri, 14 Aug 2026 17:55:30 +0200 Subject: [PATCH 1/6] =?UTF-8?q?feat(erp):=20manifeste=20d'exercice=20?= =?UTF-8?q?=E2=80=94=20d=C3=A9crire=20les=20faits=20pour=20pouvoir=20les?= =?UTF-8?q?=20rejouer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un exercice OUVERT n'a d'engagement envers personne : aucun compte déposé, aucune liasse, aucun FEC remis. Tant qu'il en va ainsi, une erreur de MODÉLISATION se corrige mieux en rejouant depuis les pièces qu'en la contournant — la piste d'audit fiable demande que l'écriture reflète sa pièce, et rejouer resserre ce lien là où une rustine le distend. extractExercise.ts ne fait que LIRE. Il produit la description ordonnée des faits : tiers, factures client et fournisseur avec leurs lignes et règlements, et l'intégralité des mouvements bancaires. Les mouvements bancaires sont le RÉFÉRENT : Qonto et Wise sont hors de Dolibarr, on ne peut pas les réécrire. Le test d'acceptation d'un rejeu est donc qu'ils soient retrouvés au centime et à la date. La ligne de partage entre ce qui se rejoue et ce qui est figé n'est pas technique mais juridique : est figé tout ce qu'un tiers détient — facture client ENVOYÉE, dépôt au greffe, déclaration transmise — et tout exercice clos. Les factures client sont donc extraites avec delivered:true : un rejeu les reproduit à l'identique, il ne les recalcule pas. Les exclusions sont DÉCLARATIVES ET MOTIVÉES, jamais codées en dur : un auditeur doit lire pourquoi une référence manque sans avoir à le deviner. Deux pièces sont écartées — FAC001-CL00001, dont la référence était malformée par un défaut de masque, et l'avoir qui l'annulait. L'opérateur a établi qu'elle n'a jamais quitté la société : KissMetrics a été facturé via Wise en février, et le document qu'il détient référence FAC001-CL0001001. L'avoir ne documentait donc qu'un faux pas interne. La séquence FAC reste continue (FAC001 à FAC008), la réémission portant elle aussi FAC001 — rien à renuméroter. Deux constats que l'extraction met au jour : - 20 des 37 mouvements bancaires portent un libellé Dolibarr générique (« SupplierInvoicePayment », « CustomerInvoicePayment ») qui traverse le grand livre tel quel et y est illisible pour un repreneur comme pour un inspecteur ; - FAF2025001 (OVH, 7,30 EUR) est datée du 24/10/2025, trois mois avant l'immatriculation, et NE FIGURE PAS à l'annexe 1 des statuts qui recense les actes accomplis pour le compte de la société en formation. Une dépense antérieure à l'existence de la société ne peut être supportée par elle que si elle est reprise par cet état ou ratifiée ensuite. Co-Authored-By: Claude Opus 5 (1M context) --- .../adc-011-tenue-comptable-et-discretion.md | 14 +- fleet/profile/exercices/2026/manifest.json | 1117 +++++++++++++++++ test/extractExercise.ts | 245 ++++ 3 files changed, 1372 insertions(+), 4 deletions(-) create mode 100644 fleet/profile/exercices/2026/manifest.json create mode 100644 test/extractExercise.ts diff --git a/fleet/profile/decisions/adc-011-tenue-comptable-et-discretion.md b/fleet/profile/decisions/adc-011-tenue-comptable-et-discretion.md index 216c0a5..bfeee6c 100644 --- a/fleet/profile/decisions/adc-011-tenue-comptable-et-discretion.md +++ b/fleet/profile/decisions/adc-011-tenue-comptable-et-discretion.md @@ -40,10 +40,16 @@ Le document le dit expressément, et les pièces sont produites sur demande. La règle est donc *pouvoir répondre simplement en cas d'enquête*, jamais *anticiper l'enquête en publiant*. -**Limite qui inverse la règle :** un flux financier entre la société et son -dirigeant se déclare, même non demandé, dès lors que le document porte -précisément sur l'absence d'un tel flux. Taire ce qui contredirait le document -n'est pas de la discrétion, c'est une omission. +**Limite qui inverse la règle — et elle est étroite :** ne se déclare, même non +demandé, que ce qui rendrait le document **faux** si on le taisait. Le critère +est la véracité de ce qui est affirmé, pas l'exhaustivité. + +Une attestation d'absence de rémunération n'a pas à mentionner une indemnité +d'occupation : celle-ci n'est pas une rémunération, elle ne contredit donc rien. +La mentionner, c'est publier une relation financière que personne n'a demandée — +et un lecteur pressé y verrait une rémunération là où il n'y en a pas. Une +première rédaction l'avait fait, en s'abritant derrière la transparence ; c'était +une erreur d'analyse : la discrétion et la sincérité ne s'opposaient pas ici. ### 2. Permanence des méthodes diff --git a/fleet/profile/exercices/2026/manifest.json b/fleet/profile/exercices/2026/manifest.json new file mode 100644 index 0000000..6fe5e16 --- /dev/null +++ b/fleet/profile/exercices/2026/manifest.json @@ -0,0 +1,1117 @@ +{ + "meta": { + "exercice": 2026, + "source": "https://erp.arcodange.lab", + "extracted_at": "2026-08-14T15:54:36.583Z", + "replayable": true, + "note": "Manifeste d'exercice — description des faits, non des écritures comptables. Les factures client sont marquées delivered:true : elles existent chez le client et se reproduisent à l'identique, jamais ne se recalculent." + }, + "exclusions": [ + { + "ref": "FAC001-CL00001", + "date": "2026-01-29", + "total_ttc": "510.00000000", + "motif": "Référence malformée (CL00001 au lieu de CL0001001) — défaut de configuration du masque. JAMAIS REMISE AU CLIENT : vérifié le 2026-08-14, aucune trace d'envoi ; KissMetrics a été facturé via Wise en février 2026, et le document qu'il détient référence FAC001-CL0001001. La séquence FAC reste continue (FAC001 à FAC008), la réémission portant elle aussi FAC001." + }, + { + "ref": "AVC001-CL0001001", + "date": "2026-01-29", + "total_ttc": "-510.00000000", + "motif": "Avoir émis le 24/02/2026 pour annuler FAC001-CL00001. Sans objet dès lors que la facture annulée n'a jamais quitté la société : il ne documentait qu'un faux pas interne, non un événement commercial. La séquence AVC devient vide." + } + ], + "anomalies": { + "libelles_generiques": 20, + "detail": "lignes bancaires portant un libellé Dolibarr générique — à remplacer au rejeu" + }, + "tiers": [ + { + "id": "1", + "name": "KissMetrics", + "code_client": "CL0001", + "code_fournisseur": null, + "client": "1", + "fournisseur": "0", + "country_id": "11", + "tva_intra": "", + "code_compta": null, + "code_compta_fournisseur": null + }, + { + "id": "2", + "name": "Wise Europe SA", + "code_client": null, + "code_fournisseur": "FO0001", + "client": "0", + "fournisseur": "1", + "country_id": "2", + "tva_intra": "", + "code_compta": null, + "code_compta_fournisseur": "401WISEEU" + }, + { + "id": "3", + "name": "Greffe du tribunal de commerce Évry", + "code_client": null, + "code_fournisseur": "FO0002", + "client": "0", + "fournisseur": "1", + "country_id": "1", + "tva_intra": "FR09326467370", + "code_compta": null, + "code_compta_fournisseur": "401GREFFE" + }, + { + "id": "4", + "name": "YOLAW", + "code_client": null, + "code_fournisseur": "FO0003", + "client": "0", + "fournisseur": "1", + "country_id": "1", + "tva_intra": "FR18753892926", + "code_compta": null, + "code_compta_fournisseur": "401YOLAW" + }, + { + "id": "5", + "name": "Medialex", + "code_client": null, + "code_fournisseur": "FO0004", + "client": "0", + "fournisseur": "1", + "country_id": "1", + "tva_intra": "", + "code_compta": null, + "code_compta_fournisseur": "401MEDIAL" + }, + { + "id": "6", + "name": "Qonto", + "code_client": null, + "code_fournisseur": "FO0005", + "client": "0", + "fournisseur": "1", + "country_id": "1", + "tva_intra": "FR10819489626", + "code_compta": null, + "code_compta_fournisseur": "401QONTO" + }, + { + "id": "7", + "name": "OVH", + "code_client": null, + "code_fournisseur": "FO0006", + "client": "0", + "fournisseur": "1", + "country_id": "1", + "tva_intra": "FR22424761419", + "code_compta": null, + "code_compta_fournisseur": "401OVH" + }, + { + "id": "8", + "name": "La Poste", + "code_client": null, + "code_fournisseur": "FO0007", + "client": "0", + "fournisseur": "1", + "country_id": "1", + "tva_intra": "FR39356000000", + "code_compta": null, + "code_compta_fournisseur": "401LAPOST" + }, + { + "id": "9", + "name": "Infogreffe", + "code_client": null, + "code_fournisseur": "FO0008", + "client": "0", + "fournisseur": "1", + "country_id": "1", + "tva_intra": "FR69338885718", + "code_compta": null, + "code_compta_fournisseur": "401INFOGR" + }, + { + "id": "10", + "name": "Darnis Operations", + "code_client": null, + "code_fournisseur": "FO0009", + "client": "0", + "fournisseur": "1", + "country_id": "1", + "tva_intra": "FR51920267531", + "code_compta": null, + "code_compta_fournisseur": "401DARNIS" + }, + { + "id": "11", + "name": "Anthropic Ireland Limited", + "code_client": null, + "code_fournisseur": "FO0010", + "client": "0", + "fournisseur": "1", + "country_id": null, + "tva_intra": "", + "code_compta": null, + "code_compta_fournisseur": "401ANTHRO" + }, + { + "id": "12", + "name": "Mistral AI SAS", + "code_client": null, + "code_fournisseur": "FO0011", + "client": "0", + "fournisseur": "1", + "country_id": null, + "tva_intra": "", + "code_compta": null, + "code_compta_fournisseur": "401MISTRA" + } + ], + "invoices": [ + { + "ref": "FAC001-CL0001001", + "socid": "1", + "date": "2026-01-29", + "date_lim_reglement": "2026-02-08", + "type": "0", + "total_ht": "510.00000000", + "total_tva": "0.00000000", + "total_ttc": "510.00000000", + "multicurrency_code": "EUR", + "statut": "2", + "paye": "1", + "delivered": true, + "lines": [ + { + "desc": "TVA non applicable – Article 259-1 du CGI – Prestation de services localisée hors de France (USA)", + "subprice": "510.00000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "510.00000000", + "type": "VIR", + "date": "2026-02-05 12:00:00", + "ref": "REC002-####002" + } + ] + }, + { + "ref": "FAC002-CL0001002", + "socid": "1", + "date": "2026-02-23", + "date_lim_reglement": "2026-03-30", + "type": "0", + "total_ht": "5100.00000000", + "total_tva": "0.00000000", + "total_ttc": "5100.00000000", + "multicurrency_code": "EUR", + "statut": "2", + "paye": "1", + "delivered": true, + "lines": [ + { + "desc": "TVA non applicable – Article 259-1 du CGI – Prestation de services localisée hors de France (USA)", + "subprice": "510.00000000", + "qty": "10", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "5100.00000000", + "type": "VIR", + "date": "2026-03-12 12:00:00", + "ref": "REC003-CL00002" + } + ] + }, + { + "ref": "FAC003-CL0001003", + "socid": "1", + "date": "2026-02-23", + "date_lim_reglement": "2026-03-30", + "type": "0", + "total_ht": "2550.00000000", + "total_tva": "0.00000000", + "total_ttc": "2550.00000000", + "multicurrency_code": "EUR", + "statut": "2", + "paye": "1", + "delivered": true, + "lines": [ + { + "desc": "TVA non applicable – Article 259-1 du CGI – Prestation de services localisée hors de France (USA)", + "subprice": "510.00000000", + "qty": "5", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "2550.00000000", + "type": "VIR", + "date": "2026-04-20 12:00:00", + "ref": "REC001-CL0001001" + } + ] + }, + { + "ref": "FAC004-CL0001004", + "socid": "1", + "date": "2026-05-22", + "date_lim_reglement": "2026-06-29", + "type": "0", + "total_ht": "2145.92000000", + "total_tva": "0.00000000", + "total_ttc": "2145.92000000", + "multicurrency_code": "EUR", + "statut": "2", + "paye": "1", + "delivered": true, + "lines": [ + { + "desc": "Conseil et accompagnement infrastructure cloud - Cycle M1 (part fixe) - periode du 23/04/2026 au 23/05/2026.\nTVA non applicable - Articles 259, 1 et 283-2 du CGI - prestation de services a un preneur assujetti etabli hors UE (Etats-Unis).", + "subprice": "2145.92000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "2145.92000000", + "type": "VIR", + "date": "2026-05-29 00:00:00", + "ref": "REC002-######001" + } + ] + }, + { + "ref": "FAC005-CL0001005", + "socid": "1", + "date": "2026-05-22", + "date_lim_reglement": "2026-08-22", + "type": "0", + "total_ht": "2575.11000000", + "total_tva": "0.00000000", + "total_ttc": "2575.11000000", + "multicurrency_code": "EUR", + "statut": "1", + "paye": "0", + "delivered": true, + "lines": [ + { + "desc": "Conseil et accompagnement infrastructure cloud - Cycle M1 (part differee) - periode du 23/04/2026 au 23/05/2026.\nPaiement differe a 3 mois - echeance 23/08/2026.\nTVA non applicable - Articles 259, 1 et 283-2 du CGI - prestation de services a un preneur assujetti etabli hors UE (Etats-Unis).", + "subprice": "2575.11000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [] + }, + { + "ref": "FAC006-CL0001006", + "socid": "1", + "date": "2026-06-22", + "date_lim_reglement": "2026-07-30", + "type": "0", + "total_ht": "2145.92000000", + "total_tva": "0.00000000", + "total_ttc": "2145.92000000", + "multicurrency_code": "EUR", + "statut": "2", + "paye": "1", + "delivered": true, + "lines": [ + { + "desc": "Conseil et accompagnement infrastructure cloud - Cycle M2 (part fixe) - periode du 23/05/2026 au 23/06/2026.\nTVA non applicable - Articles 259, 1 et 283-2 du CGI - prestation de services a un preneur assujetti etabli hors UE (Etats-Unis).", + "subprice": "2145.92000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "2145.92000000", + "type": "VIR", + "date": "2026-06-25 00:00:00", + "ref": "REC002-######002" + } + ] + }, + { + "ref": "FAC007-CL0001007", + "socid": "1", + "date": "2026-06-22", + "date_lim_reglement": "2026-09-22", + "type": "0", + "total_ht": "2575.11000000", + "total_tva": "0.00000000", + "total_ttc": "2575.11000000", + "multicurrency_code": "EUR", + "statut": "1", + "paye": "0", + "delivered": true, + "lines": [ + { + "desc": "Conseil et accompagnement infrastructure cloud - Cycle M2 (part differee) - periode du 23/05/2026 au 23/06/2026.\nPaiement differe a 3 mois - echeance 23/09/2026.\nTVA non applicable - Articles 259, 1 et 283-2 du CGI - prestation de services a un preneur assujetti etabli hors UE (Etats-Unis).", + "subprice": "2575.11000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [] + }, + { + "ref": "FAC008-CL0001008", + "socid": "1", + "date": "2026-07-22", + "date_lim_reglement": "2026-08-21", + "type": "0", + "total_ht": "2185.00000000", + "total_tva": "0.00000000", + "total_ttc": "2185.00000000", + "multicurrency_code": "EUR", + "statut": "2", + "paye": "1", + "delivered": true, + "lines": [ + { + "desc": "Conseil et accompagnement infrastructure cloud — Cycle M3 (part fixe) — période d'exécution du 23/06/2026 au 23/07/2026. Contrat cadre du 23/04/2026, art. 6 ; avenant art. 2 et 4. Montant contractuel 2 500,00 USD converti à 2 185,00 € (taux du jour du paiement).\nPrestation de services au sens des articles 259, 1° et 283-2 du Code général des impôts. TVA française non applicable — preneur assujetti établi hors de l'Union européenne (États-Unis). Auto-liquidation par le preneur dans son pays d'établissement le cas échéant. / Services rendered pursuant to Articles 259, 1° and 283-2 of the French General Tax Code. French VAT not applicable — recipient is a taxable person established outside the European Union (United States). Reverse charge by the recipient in its country of establishment if applicable.", + "subprice": "2185.00000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "2185.00000000", + "type": "VIR", + "date": "2026-07-20 00:00:00", + "ref": "REC003-######003" + } + ] + } + ], + "supplierInvoices": [ + { + "ref": "FAF2026001", + "ref_supplier": "PLAN_ORDER_CHECKOUT-invoice-23917601", + "socid": "2", + "date": "2026-01-25", + "total_ht": "50.00000000", + "total_tva": "0.00000000", + "total_ttc": "50.00000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Ouverture compte - Exonération de TVA – Article 261 C du CGI", + "subprice": "50.00000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "50.00000000", + "type": "VIR", + "date": "2026-01-26 12:00:00", + "ref": "REF2026001" + } + ] + }, + { + "ref": "FAF2026002", + "ref_supplier": "26004975", + "socid": "3", + "date": "2026-01-08", + "total_ht": "47.69000000", + "total_tva": "9.54000000", + "total_ttc": "57.23000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Déclaration relative au bénéficiaire effectif lors de la demande
\r\nd'immatriculation 16,951", + "subprice": "16.95000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + }, + { + "desc": "Immatriculation Personne Morale sur création", + "subprice": "23.30000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + }, + { + "desc": "Dépôts d'actes ou de pièces", + "subprice": "6.36000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + }, + { + "desc": "Acte de greffe (Réclamation de renseignements ou pièces manquantes (C.
\r\ncom., R 123-97 al 2))", + "subprice": "1.08000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "55.93000000", + "type": "CB", + "date": "2026-01-09 12:00:00", + "ref": "REF2026002" + }, + { + "amount": "1.30000000", + "type": "CB", + "date": "2026-01-15 12:00:00", + "ref": "REF2026003" + } + ] + }, + { + "ref": "FAF2026003", + "ref_supplier": "1091237202601", + "socid": "4", + "date": "2026-01-03", + "total_ht": "1.66000000", + "total_tva": "0.33000000", + "total_ttc": "1.99000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Modèle Statuts EURL", + "subprice": "1.66000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "1.99000000", + "type": "CB", + "date": "2026-01-04 12:00:00", + "ref": "REF2026004" + } + ] + }, + { + "ref": "FAF2026004", + "ref_supplier": "74389798", + "socid": "5", + "date": "2026-01-08", + "total_ht": "124.00000000", + "total_tva": "24.80000000", + "total_ttc": "148.80000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "AVIS DE CONSTITUTION SARLU ARCODANGE
\r\nIdentifiant annonce: 22565141
\r\nparution le 10/01/2026 sur 20 Minutes Essonne", + "subprice": "124.00000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "148.80000000", + "type": "CB", + "date": "2026-01-09 12:00:00", + "ref": "REF2026005" + } + ] + }, + { + "ref": "FAF2026005", + "ref_supplier": "06-01-26-payment-366753", + "socid": "6", + "date": "2026-01-05", + "total_ht": "169.00000000", + "total_tva": "33.80000000", + "total_ttc": "202.80000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Pack Qonto Basic
\r\nVotre pack inclut :
\r\n- Prestation dépôt de capital en ligne- 12 mois d'abonnement Qonto au
\r\nforfait Basic (à partir de la date devérification réglementaire de votre
\r\nnouvelle entreprise", + "subprice": "169.00000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "202.80000000", + "type": "CB", + "date": "2026-01-06 12:00:00", + "ref": "REF2026006" + } + ] + }, + { + "ref": "FAF2026006", + "ref_supplier": "210000003184C", + "socid": "8", + "date": "2026-01-11", + "total_ht": "8.43000000", + "total_tva": "0.00000000", + "total_ttc": "8.43000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Lettre recommandée: 22 g
\r\nExonération de TVA, article 261-4-1° du CGI", + "subprice": "8.43000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "8.43000000", + "type": "CB", + "date": "2026-01-12 12:00:00", + "ref": "REF2026007" + } + ] + }, + { + "ref": "FAF2026007", + "ref_supplier": "60117-ZMHMT", + "socid": "9", + "date": "2026-01-16", + "total_ht": "2.67000000", + "total_tva": "0.53000000", + "total_ttc": "3.20000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Extrait KBIS et frais d'emission
\r\nFormat electronique", + "subprice": "2.67000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "3.20000000", + "type": "CB", + "date": "2026-01-17 12:00:00", + "ref": "REF2026008" + } + ] + }, + { + "ref": "FAF2026008", + "ref_supplier": "F1040", + "socid": "10", + "date": "2026-02-27", + "total_ht": "510.00000000", + "total_tva": "102.00000000", + "total_ttc": "612.00000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Apport d'affaire Arcodange ", + "subprice": "510.00000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "612.00000000", + "type": "VIR", + "date": "2026-03-13 12:00:00", + "ref": "REF2026009" + } + ] + }, + { + "ref": "FAF2026009", + "ref_supplier": "F1042", + "socid": "10", + "date": "2026-02-27", + "total_ht": "255.00000000", + "total_tva": "51.00000000", + "total_ttc": "306.00000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Apport d'affaire Arcodange ", + "subprice": "510.00000000", + "qty": "0.5", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "306.00000000", + "type": "VIR", + "date": "2026-05-10 12:00:00", + "ref": "REF2026010" + } + ] + }, + { + "ref": "FAF2026010", + "ref_supplier": "F1046", + "socid": "10", + "date": "2026-06-28", + "total_ht": "219.50000000", + "total_tva": "43.90000000", + "total_ttc": "263.40000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Apport d affaire Arcodange - Projet — juin 2026 (Hiway F1046)", + "subprice": "219.50000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "263.40000000", + "type": "VIR", + "date": "2026-06-29 00:00:00", + "ref": "REF2026012" + } + ] + }, + { + "ref": "FAF2026011", + "ref_supplier": "9BF0758D-695749", + "socid": "11", + "date": "2026-04-11", + "total_ht": "180.00000000", + "total_tva": "0.00000000", + "total_ttc": "180.00000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Claude Pro — abonnement avril 2026 (facture 9BF0758D-695749, autoliquidation UE)", + "subprice": "180.00000000", + "qty": "1", + "tva_tx": "0.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "180.00000000", + "type": "CB", + "date": "2026-04-13 00:00:00", + "ref": "REF2026014" + } + ] + }, + { + "ref": "FAF2026012", + "ref_supplier": "MSTRL-API-814045-001", + "socid": "12", + "date": "2026-04-01", + "total_ht": "143.90000000", + "total_tva": "28.78000000", + "total_ttc": "172.68000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "", + "subprice": "0.00000000", + "qty": "0", + "tva_tx": "0.0000", + "product_type": "0" + } + ], + "payments": [ + { + "amount": "172.68000000", + "type": "CB", + "date": "2026-04-03 00:00:00", + "ref": "REF2026013" + } + ] + }, + { + "ref": "FAF2026013", + "ref_supplier": "F1045", + "socid": "10", + "date": "2026-06-29", + "total_ht": "214.70000000", + "total_tva": "42.94000000", + "total_ttc": "257.64000000", + "statut": "2", + "paye": "1", + "lines": [ + { + "desc": "Apport d'affaire Arcodange - Projet — juin 2026 (facture Hiway F1045 du 30/06/2026)", + "subprice": "214.70000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [ + { + "amount": "257.64000000", + "type": "VIR", + "date": "2026-06-29 00:00:00", + "ref": "REF2026011" + } + ] + }, + { + "ref": "FAF2026014", + "ref_supplier": "F1048", + "socid": "10", + "date": "2026-08-30", + "total_ht": "218.50000000", + "total_tva": "43.70000000", + "total_ttc": "262.20000000", + "statut": "1", + "paye": "0", + "lines": [ + { + "desc": "Apport d'affaire Arcodange — commission 10 % sur encaissement KM du 20/07/2026", + "subprice": "218.50000000", + "qty": "1", + "tva_tx": "20.0000", + "product_type": "1" + } + ], + "payments": [] + } + ], + "accounts": [ + { + "id": "1", + "label": "QONTO", + "ref": "QON1", + "account_number": "51211", + "accountancy_journal": "BQ1", + "currency_code": "EUR", + "balance": 4158.28, + "lines": [ + { + "date": "2026-01-06", + "amount": 1000, + "label": "(Solde initial)", + "generic_label": false, + "num_releve": "202601" + }, + { + "date": "2026-01-25", + "amount": -50, + "label": "ouverture compte WISE", + "generic_label": false, + "num_releve": "202601" + }, + { + "date": "2026-03-12", + "amount": 5000, + "label": "virement du compte international vers le compte principal", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-03-12", + "amount": -612, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-04-02", + "amount": -172.68, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-04-12", + "amount": -180, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-05-09", + "amount": -306, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-06-28", + "amount": -257.64, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-06-28", + "amount": -263.4, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + } + ] + }, + { + "id": "2", + "label": "WISE EURO", + "ref": "WIS2", + "account_number": "51212", + "accountancy_journal": "BQ2", + "currency_code": "EUR", + "balance": 9636.84, + "lines": [ + { + "date": "2026-01-25", + "amount": 0, + "label": "Solde initial", + "generic_label": false, + "num_releve": "202601" + }, + { + "date": "2026-01-25", + "amount": 50, + "label": "ouverture compte WISE", + "generic_label": false, + "num_releve": "202601" + }, + { + "date": "2026-01-25", + "amount": -50, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": "202601" + }, + { + "date": "2026-02-04", + "amount": 510, + "label": "", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-02-04", + "amount": 510, + "label": "", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-02-04", + "amount": -510, + "label": "(CustomerInvoicePaymentBack)", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-03-11", + "amount": 5100, + "label": "(CustomerInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-03-12", + "amount": -5000, + "label": "virement du compte international vers le compte principal", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-04-19", + "amount": 2550, + "label": "(CustomerInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-05-28", + "amount": 2145.92, + "label": "(CustomerInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-06-24", + "amount": 2145.92, + "label": "(CustomerInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-07-19", + "amount": 2185, + "label": "(CustomerInvoicePayment)", + "generic_label": true, + "num_releve": null + } + ] + }, + { + "id": "3", + "label": "G.RADUREAU Compte Courant Asso", + "ref": "CCA1", + "account_number": "45511", + "accountancy_journal": "CCA1", + "currency_code": "EUR", + "balance": -1912.98, + "lines": [ + { + "date": "2025-10-23", + "amount": -7.3, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-01-03", + "amount": -1.99, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-01-05", + "amount": -202.8, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-01-08", + "amount": -55.93, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-01-08", + "amount": -148.8, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-01-11", + "amount": -8.43, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-01-14", + "amount": -1.3, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-01-15", + "amount": 0, + "label": "Solde initial", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-01-16", + "amount": -3.2, + "label": "(SupplierInvoicePayment)", + "generic_label": true, + "num_releve": null + }, + { + "date": "2026-01-30", + "amount": -163.23, + "label": "Indemnité d'occupation — janvier 2026", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-02-27", + "amount": -220, + "label": "Indemnité d'occupation — février 2026", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-03-30", + "amount": -220, + "label": "Indemnité d'occupation — mars 2026", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-04-29", + "amount": -220, + "label": "Indemnité d'occupation — avril 2026", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-05-30", + "amount": -220, + "label": "Indemnité d'occupation — mai 2026", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-06-29", + "amount": -220, + "label": "Indemnité d'occupation — juin 2026", + "generic_label": false, + "num_releve": null + }, + { + "date": "2026-07-30", + "amount": -220, + "label": "Indemnité d'occupation — juillet 2026", + "generic_label": false, + "num_releve": null + } + ] + } + ] +} \ No newline at end of file diff --git a/test/extractExercise.ts b/test/extractExercise.ts new file mode 100644 index 0000000..b92264c --- /dev/null +++ b/test/extractExercise.ts @@ -0,0 +1,245 @@ +/* + Extrait d'une instance Dolibarr le MANIFESTE D'UN EXERCICE : la description + complète et ordonnée des faits qui le composent. + + POURQUOI. Un exercice ouvert n'a d'engagement envers personne — aucun compte + déposé, aucune liasse, aucun FEC remis. Tant qu'il en va ainsi, une erreur de + MODÉLISATION (mauvais compte, auxiliaire manquant, libellé illisible) se + corrige mieux en rejouant depuis les pièces qu'en la contournant : la piste + d'audit fiable demande que l'écriture reflète sa pièce, et rejouer resserre ce + lien là où une rustine le distend. + + Ce fichier ne fait que LIRE. Il produit le manifeste ; le rejeu est un autre + outil, et il ne s'exécute que sur une cible vierge. + + CE QUI EST REJOUABLE, ET CE QUI NE L'EST PAS. La ligne de partage n'est pas + technique, elle est juridique : + + rejouable : factures fournisseur enregistrées, lignes bancaires, paiements + divers, charges sociales, tiers — tout ce qui est interne ; + FIGÉ : toute pièce déjà remise à un tiers (facture client ENVOYÉE, + dépôt au greffe, déclaration transmise) et tout exercice clos. + + Une facture partie chez le client existe chez lui : aucun rejeu ne l'efface. + Le manifeste les extrait quand même — il décrit l'exercice — mais les marque + `delivered: true`, et le rejeu devra les reproduire à l'identique plutôt que + les recalculer. + + LE RÉFÉRENT DE VÉRIFICATION. Les mouvements bancaires sont la seule partie de + l'exercice qu'on ne peut pas réécrire : Qonto et Wise sont hors de Dolibarr. + Le manifeste les embarque donc intégralement, et le test d'acceptation d'un + rejeu est qu'ils soient retrouvés au centime et à la date. + + Usage : + DENO_TLS_CA_STORE=system \ + DOLIBARR_URL=https://erp.arcodange.lab DOLAPIKEY=$(cat test/....key) \ + deno run -A test/extractExercise.ts --year 2026 --out manifest.json + + `DENO_TLS_CA_STORE=system` n'est pas décoratif : les hôtes du lab portent un + certificat signé par une autorité interne, présente dans le trousseau système + mais absente du magasin que Deno embarque. Sans lui, `fetch` échoue sur + « invalid peer certificate: UnknownIssuer » — alors que curl et Playwright, + qui lisent le trousseau système, passent sans rien dire. +*/ +import "load_dotenv"; + +const argv = Deno.args; +const pick = (f: string, d = "") => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : d); + +const base = pick("--url", Deno.env.get("DOLIBARR_URL") ?? "https://erp.arcodange.lab"); +const key = Deno.env.get("DOLAPIKEY") ?? ""; +const year = Number(pick("--year", "2026")); +const out = pick("--out", `manifest-${year}.json`); + +/** + * Pièces volontairement écartées du rejeu, avec leur motif. + * + * Une exclusion n'est légitime que si la pièce N'A JAMAIS ÉTÉ REMISE À UN TIERS. + * Le motif est consigné dans le manifeste : un auditeur doit pouvoir lire + * pourquoi une référence manque, sans avoir à le deviner ni à nous croire. + */ +const EXCLUSIONS: Record = { + "FAC001-CL00001": + "Référence malformée (CL00001 au lieu de CL0001001) — défaut de configuration du masque. " + + "JAMAIS REMISE AU CLIENT : vérifié le 2026-08-14, aucune trace d'envoi ; KissMetrics a été " + + "facturé via Wise en février 2026, et le document qu'il détient référence FAC001-CL0001001. " + + "La séquence FAC reste continue (FAC001 à FAC008), la réémission portant elle aussi FAC001.", + "AVC001-CL0001001": + "Avoir émis le 24/02/2026 pour annuler FAC001-CL00001. Sans objet dès lors que la facture " + + "annulée n'a jamais quitté la société : il ne documentait qu'un faux pas interne, non un " + + "événement commercial. La séquence AVC devient vide.", +}; + +if (!key) { + console.error("DOLAPIKEY requis (lecture seule)."); + Deno.exit(2); +} + +async function api(path: string): Promise { + const r = await fetch(`${base}/api/index.php${path}`, { headers: { DOLAPIKEY: key } }); + if (!r.ok) return null; + try { + return await r.json() as T; + } catch { + return null; + } +} + +/** Dolibarr rend des epochs ; on les fige en ISO dans le fuseau du serveur. */ +const iso = (ts: unknown): string | null => { + const n = Number(ts); + if (!n) return null; + return new Date(n * 1000).toISOString().slice(0, 10); +}; + +const inYear = (ts: unknown) => { + const d = iso(ts); + return !!d && d.startsWith(String(year)); +}; + +type Any = Record; + +console.log(`source : ${base}`); +console.log(`exercice : ${year}\n`); + +// --- tiers ------------------------------------------------------------------ +const tiers = (await api("/thirdparties?limit=500") ?? []).map((t) => ({ + id: t.id, + name: t.name, + code_client: t.code_client, + code_fournisseur: t.code_fournisseur, + client: t.client, + fournisseur: t.fournisseur, + country_id: t.country_id, + tva_intra: t.tva_intra, + code_compta: t.code_compta, + code_compta_fournisseur: t.code_compta_fournisseur, +})); +console.log(`tiers : ${tiers.length}`); + +// --- factures client -------------------------------------------------------- +// `delivered: true` : elles sont parties chez le client. Un rejeu les reproduit +// à l'identique — référence, date, montant — il ne les recalcule pas. +const invoices = []; +const excluded: Any[] = []; +for (const f of await api("/invoices?limit=500") ?? []) { + if (!inYear(f.date)) continue; + const motif = EXCLUSIONS[String(f.ref)]; + if (motif) { + excluded.push({ ref: f.ref, date: iso(f.date), total_ttc: f.total_ttc, motif }); + continue; + } + const payments = await api(`/invoices/${f.id}/payments`) ?? []; + invoices.push({ + ref: f.ref, + socid: f.socid, + date: iso(f.date), + date_lim_reglement: iso(f.date_lim_reglement), + type: f.type, + total_ht: f.total_ht, + total_tva: f.total_tva, + total_ttc: f.total_ttc, + multicurrency_code: f.multicurrency_code, + statut: f.statut, + paye: f.paye, + delivered: true, + lines: (f.lines as Any[] ?? []).map((l) => ({ + desc: l.desc, subprice: l.subprice, qty: l.qty, tva_tx: l.tva_tx, product_type: l.product_type, + })), + payments: payments.map((p) => ({ amount: p.amount, type: p.type, date: p.date, ref: p.ref })), + }); +} +console.log(`factures client : ${invoices.length} (figées — déjà remises au client)`); +for (const e of excluded) console.log(` ⊘ exclue : ${e.ref} (${e.total_ttc} EUR) — ${String(e.motif).slice(0, 72)}…`); + +// --- factures fournisseur --------------------------------------------------- +const supplierInvoices = []; +for (const f of await api("/supplierinvoices?limit=500") ?? []) { + if (!inYear(f.date)) continue; + const payments = await api(`/supplierinvoices/${f.id}/payments`) ?? []; + supplierInvoices.push({ + ref: f.ref, + ref_supplier: f.ref_supplier, + socid: f.socid, + date: iso(f.date), + total_ht: f.total_ht, + total_tva: f.total_tva, + total_ttc: f.total_ttc, + statut: f.statut, + paye: f.paye, + lines: (f.lines as Any[] ?? []).map((l) => ({ + desc: l.desc, subprice: l.subprice, qty: l.qty, tva_tx: l.tva_tx, product_type: l.product_type, + })), + payments: payments.map((p) => ({ amount: p.amount, type: p.type, date: p.date, ref: p.ref })), + }); +} +console.log(`factures fournisseur : ${supplierInvoices.length}`); + +// --- comptes et mouvements bancaires --------------------------------------- +// LE RÉFÉRENT. Tout le reste se vérifie contre ces lignes. +const accounts = []; +let bankLineCount = 0; +for (const a of await api("/bankaccounts") ?? []) { + const lines = (await api(`/bankaccounts/${a.id}/lines?limit=1000`) ?? []) + .map((l) => ({ + date: iso(l.datev ?? l.dateo), + amount: Number(l.amount), + label: l.label, + // Les libellés générés par Dolibarr — « SupplierInvoicePayment », + // « CustomerInvoicePayment » — traversent le grand livre tels quels et y + // sont illisibles. Le rejeu doit les remplacer par le libellé de la pièce. + generic_label: /^\(?(Customer|Supplier)InvoicePayment\)?$/.test(String(l.label ?? "")), + num_releve: l.num_releve, + })) + .filter((l) => l.date) + .sort((x, y) => String(x.date).localeCompare(String(y.date))); + bankLineCount += lines.length; + accounts.push({ + id: a.id, + label: a.label, + ref: a.ref, + account_number: a.account_number, + accountancy_journal: a.accountancy_journal, + currency_code: a.currency_code, + balance: Number(lines.reduce((s, l) => s + l.amount, 0).toFixed(2)), + lines, + }); +} +console.log(`comptes bancaires : ${accounts.length} (${bankLineCount} mouvements — référent de vérification)`); + +const generics = accounts.flatMap((a) => a.lines).filter((l) => l.generic_label).length; + +const manifest = { + meta: { + exercice: year, + source: base, + extracted_at: new Date().toISOString(), + // Un exercice clos ne se rejoue pas : la clôture est la limite dure. + replayable: true, + note: "Manifeste d'exercice — description des faits, non des écritures comptables. " + + "Les factures client sont marquées delivered:true : elles existent chez le client " + + "et se reproduisent à l'identique, jamais ne se recalculent.", + }, + exclusions: excluded, + anomalies: { + libelles_generiques: generics, + detail: generics + ? "lignes bancaires portant un libellé Dolibarr générique — à remplacer au rejeu" + : "aucune", + }, + tiers, + invoices, + supplierInvoices, + accounts, +}; + +await Deno.writeTextFile(out, JSON.stringify(manifest, null, 2)); + +const totalClient = invoices.reduce((s, f) => s + Number(f.total_ttc ?? 0), 0); +const totalFourn = supplierInvoices.reduce((s, f) => s + Number(f.total_ttc ?? 0), 0); +console.log(`\n--- récapitulatif ---`); +console.log(` produits facturés : ${totalClient.toFixed(2)} EUR`); +console.log(` achats facturés : ${totalFourn.toFixed(2)} EUR`); +for (const a of accounts) console.log(` ${String(a.label).padEnd(32)} ${a.balance.toFixed(2)} EUR`); +console.log(` libellés génériques : ${generics} ← à corriger au rejeu`); +console.log(`\nmanifeste écrit : ${out}`); -- 2.54.0 From b49cbd77a9db86e7b46a1b4ff597a4608018b7be Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Fri, 14 Aug 2026 18:45:09 +0200 Subject: [PATCH 2/6] =?UTF-8?q?feat(erp):=20rejeu=20=C3=A0=20blanc=20?= =?UTF-8?q?=E2=80=94=20le=20manifeste=20reproduit=20la=20banque=20au=20cen?= =?UTF-8?q?time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 37/37 mouvements appariés à la date et au montant sur les trois comptes. Le manifeste décrit l'exercice exactement comme la banque le connaît, et la banque est le seul référent qu'on ne peut pas réécrire : Qonto et Wise sont hors de Dolibarr. C'est le test qui compte, et il passe. L'appariement se fait sur (date, montant) et SUR RIEN D'AUTRE. Le libellé n'y participe pas — c'est précisément ce que le rejeu corrige, il ne peut pas servir à valider. Les corrections sont DÉCLARATIVES : chacune nomme le défaut, sa cause, son remède et sa portée. Une correction sans cause écrite est une réécriture silencieuse, et c'est ce qu'un repreneur lira pour comprendre pourquoi le livre reconstruit diffère de l'ancien. - 20 des 37 mouvements portent un libellé Dolibarr générique, illisible au grand livre ; - les 7 indemnités ne se journalisent pas faute de compte auxiliaire — le correctif (455110) est vérifié en sandbox ; - la TVA d'OVH est réclamée en déduction alors que la facture est au nom personnel du fondateur : 7,30 TTC au taux 0, déductible 353,76 → 352,54. Aucune déclaration déposée (CA12 annuelle), la correction est gratuite. CE QUI RESTE BLOQUÉ, et ce n'est pas un détail d'implémentation : un rejeu EN ÉCRITURE exige une instance VIERGE. sandbox-lifecycle.sh ne sait que refresh-from-prod — un re-seed iso-prod, qui réimporte précisément les défauts qu'on veut corriger. Écrire le rejeu sans cette capacité produirait des doublons, pas une reconstruction. Le mode vierge est le préalable. Co-Authored-By: Claude Opus 5 (1M context) --- test/replayExercise.ts | 151 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 test/replayExercise.ts diff --git a/test/replayExercise.ts b/test/replayExercise.ts new file mode 100644 index 0000000..b8b4fe2 --- /dev/null +++ b/test/replayExercise.ts @@ -0,0 +1,151 @@ +/* + Rejeu d'un exercice à partir de son manifeste — pour l'instant EN LECTURE SEULE. + + CE QU'IL FAIT AUJOURD'HUI. Il calcule l'état que produirait le rejeu, corrections + comprises, et le confronte au RÉFÉRENT : les mouvements bancaires. Qonto et Wise + sont hors de Dolibarr, on ne peut pas les réécrire ; si le rejeu les reproduit au + centime et à la date, il décrit le même exercice. C'est le seul test qui vaille. + + CE QU'IL NE FAIT PAS ENCORE, et pourquoi. Un rejeu en écriture suppose une + instance VIERGE. `ops/sandbox/sandbox-lifecycle.sh` ne sait que `refresh-from-prod` + — un re-seed iso-prod, qui réimporte précisément les défauts qu'on veut corriger. + Écrire le rejeu sans cette capacité produirait des doublons, pas une reconstruction. + La capacité manquante est donc un préalable, pas un détail d'implémentation. + + LES CORRECTIONS QUE LE REJEU APPLIQUERAIT. Elles sont déclarées ici, pas éparpillées + dans le code : chacune nomme le défaut, sa cause et son remède. C'est ce que lira + un comptable repreneur pour comprendre pourquoi le livre reconstruit diffère de + l'ancien. + + Usage : + DENO_TLS_CA_STORE=system DOLAPIKEY=… \ + deno run -A test/replayExercise.ts --manifest …/manifest.json [--url …] +*/ +import "load_dotenv"; + +const argv = Deno.args; +const pick = (f: string, d = "") => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : d); + +const manifestPath = pick("--manifest", "manifest-2026.json"); +const base = pick("--url", Deno.env.get("DOLIBARR_URL") ?? "https://erp.arcodange.lab"); +const key = Deno.env.get("DOLAPIKEY") ?? ""; + +type Any = Record; +type Line = { date: string; amount: number; label: string; generic_label: boolean }; +type Account = { id: string; label: string; balance: number; lines: Line[] }; + +const manifest = JSON.parse(await Deno.readTextFile(manifestPath)) as { + meta: Any; exclusions: Any[]; accounts: Account[]; + invoices: Any[]; supplierInvoices: Any[]; tiers: Any[]; +}; + +/** + * Corrections que le rejeu appliquerait. Déclaratives : le défaut, sa cause, son + * remède. Une correction sans cause écrite est une réécriture silencieuse. + */ +const CORRECTIONS = [ + { + id: "libelles-generiques", + defaut: "20 des 37 mouvements bancaires portent « (SupplierInvoicePayment) » ou " + + "« (CustomerInvoicePayment) », libellés générés par Dolibarr", + cause: "aucun libellé n'est fourni à l'enregistrement du règlement ; Dolibarr retombe " + + "sur une clé de traduction", + remede: "libeller chaque règlement d'après sa pièce — référence du fournisseur ou du client, " + + "objet, période", + portee: "grand livre : ces libellés le traversent tels quels et y sont illisibles pour un " + + "repreneur comme pour un inspecteur", + }, + { + id: "auxiliaire-indemnites", + defaut: "les 7 paiements divers d'indemnité d'occupation ne se journalisent pas", + cause: "compte auxiliaire vide sur la ligne de charge ; le journal de banque exige une " + + "contrepartie identifiée (« Tiers inconnu et code comptable auxiliaire non défini »)", + remede: "renseigner subledger_account = 455110 — vérifié en sandbox, l'écriture journalise", + portee: "sans cela le compte courant reste hors du grand livre : 1 483,23 EUR absents", + }, + { + id: "tva-ovh-non-deductible", + defaut: "FAF2025001 enregistrée 6,08 HT + 1,22 TVA, la TVA figurant en déductible " + + "(CA3 ligne 20, période 2025-10)", + cause: "facture émise au nom personnel du fondateur, la société n'étant pas immatriculée ; " + + "une facture adressée à un particulier n'ouvre aucun droit à déduction", + remede: "enregistrer 7,30 EUR TTC au taux 0 — la charge est reprise pour son montant TTC " + + "(décision n° 3, reprise L. 210-6)", + portee: "TVA déductible de l'exercice : 353,76 → 352,54 EUR. Aucune déclaration déposée " + + "(CA12 annuelle, exercice clos le 31/12) — la correction ne coûte rien", + }, +] as const; + +async function api(path: string): Promise { + const r = await fetch(`${base}/api/index.php${path}`, { headers: { DOLAPIKEY: key } }); + if (!r.ok) return null; + try { return await r.json() as T; } catch { return null; } +} + +const iso = (ts: unknown): string | null => { + const n = Number(ts); + return n ? new Date(n * 1000).toISOString().slice(0, 10) : null; +}; + +console.log(`manifeste : ${manifestPath}`); +console.log(`exercice : ${manifest.meta.exercice}`); +console.log(`cible : ${base}\n`); + +// --- 1. le référent : la banque --------------------------------------------- +console.log("=== CONTRÔLE BANCAIRE — le rejeu reproduit-il les mouvements ? ===\n"); + +let totalAttendu = 0, totalTrouve = 0, ecarts = 0; +for (const a of manifest.accounts) { + const live = (await api(`/bankaccounts/${a.id}/lines?limit=1000`) ?? []) + .map((l) => ({ date: iso(l.datev ?? l.dateo), amount: Number(l.amount) })) + .filter((l) => l.date); + + // Appariement par (date, montant) : ce que la banque impose, et rien de plus. + // Le libellé ne participe PAS à l'appariement — c'est justement ce qu'on corrige. + const reste = [...live]; + const manquants: Line[] = []; + for (const m of a.lines) { + const i = reste.findIndex((l) => l.date === m.date && Math.abs(l.amount - m.amount) < 0.005); + if (i >= 0) reste.splice(i, 1); else manquants.push(m); + } + totalAttendu += a.lines.length; + totalTrouve += a.lines.length - manquants.length; + ecarts += manquants.length + reste.length; + + const verdict = manquants.length === 0 && reste.length === 0 ? "conforme" : "ÉCART"; + console.log(` ${a.label.padEnd(32)} ${String(a.lines.length).padStart(3)} mouvements ${a.balance.toFixed(2).padStart(10)} EUR ${verdict}`); + for (const m of manquants) console.log(` manquant à la cible : ${m.date} ${m.amount.toFixed(2)} ${m.label}`); + for (const r of reste) console.log(` en trop à la cible : ${r.date} ${r.amount.toFixed(2)}`); +} + +console.log(`\n ${totalTrouve}/${totalAttendu} mouvements appariés à la date et au centime` + + (ecarts ? ` — ${ecarts} écart(s)` : " — aucun écart")); + +// --- 2. ce que le rejeu changerait ------------------------------------------ +console.log("\n=== CORRECTIONS QUE LE REJEU APPLIQUERAIT ===\n"); +for (const c of CORRECTIONS) { + console.log(` ▸ ${c.id}`); + console.log(` défaut : ${c.defaut}`); + console.log(` cause : ${c.cause}`); + console.log(` remède : ${c.remede}`); + console.log(` portée : ${c.portee}\n`); +} + +// --- 3. ce que le rejeu écarterait ------------------------------------------ +console.log("=== PIÈCES ÉCARTÉES ===\n"); +for (const e of manifest.exclusions) { + console.log(` ⊘ ${e.ref} (${Number(e.total_ttc).toFixed(2)} EUR, ${e.date})`); + console.log(` ${String(e.motif).replace(/(.{92})/g, "$1\n ")}\n`); +} + +// --- 4. verdict -------------------------------------------------------------- +const genericCount = manifest.accounts.flatMap((a) => a.lines).filter((l) => l.generic_label).length; +console.log("=== VERDICT ===\n"); +console.log(` manifeste complet vis-à-vis de la banque : ${ecarts === 0 ? "OUI" : "NON — " + ecarts + " écart(s)"}`); +console.log(` corrections à appliquer : ${CORRECTIONS.length}`); +console.log(` libellés à réécrire : ${genericCount}`); +console.log(` pièces écartées : ${manifest.exclusions.length}`); +console.log(`\n Le rejeu EN ÉCRITURE reste bloqué : il exige une instance vierge, que`); +console.log(` sandbox-lifecycle.sh ne sait pas produire (refresh-from-prod uniquement).`); + +Deno.exit(ecarts === 0 ? 0 : 1); -- 2.54.0 From a3540982a908262e2211a6abb84b59c58a45078e Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Fri, 14 Aug 2026 19:11:18 +0200 Subject: [PATCH 3/6] feat(ops): garde de cluster sur la sauvegarde, et mode vierge (incomplet) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEUX CHOSES, dont une inachevée et dite comme telle. 1. LA SAUVEGARDE N'AVAIT AUCUNE GARDE DE CLUSTER. 24 appels kubectl nus, aucun contexte épinglé — alors que le script lit des secrets, crée des Jobs et sait RESTAURER une base. Lancé sur le contexte courant d'une station de travail, il serait parti chercher les secrets Longhorn du cluster d'un client. Même patron que ops/sandbox : ERP_KUBE_CONTEXT, wrapper K(), empreinte positive vérifiée avant tout dispatch. Testé contre le cluster client et contre un contexte inexistant : il refuse les deux. Sauvegarde de production passée dans la foulée. La base a été dédupliquée à juste titre — rien n'avait été écrit depuis la sauvegarde automatique de 01:00. Au passage, le CronJob quotidien tourne bien depuis juillet ; il était encore noté comme à faire. 2. LE MODE VIERGE PURGE MAIS NE RECONSTRUIT PAS. `blank --yes` vide la base du bac à sable (295 tables -> 0, vérifié) avec trois protections empilées : refus sans --yes, garde de cluster, et une relecture de current_database() DANS le Job lui-même — une purge sur la mauvaise base ne se rattrape pas par un refresh, contrairement à la bonne. Mais l'instance ne se reconstruit pas. Premier essai : install.lock vit sur le volume documents, que la purge ne touche pas, si bien que Dolibarr servait un login sur un schéma inexistant. Correctif appliqué — retrait du verrou puis redémarrage. Second essai : le verrou reste absent, et le schéma reste à ZÉRO table. L'entrypoint de l'image ne lance aucune installation automatique. CE QU'IL MANQUE est donc nommé dans le script : un « golden empty », pg_dump d'une instance fraîchement installée — schéma et données de référence, aucune donnée métier. `blank` le restaurerait au lieu de laisser la base vide, comme refresh-from-prod restaure le dump de production. Seule la source change. En l'état `blank` laisse le bac à sable inutilisable, et le script le dit. Le bac à sable a été remis iso-prod avant de rendre la main. Co-Authored-By: Claude Opus 5 (1M context) --- ops/backup/dolibarr-backup.sh | 73 +++++++++++++------- ops/sandbox/sandbox-lifecycle.sh | 110 ++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 25 deletions(-) diff --git a/ops/backup/dolibarr-backup.sh b/ops/backup/dolibarr-backup.sh index 5ebfbf0..f932cae 100755 --- a/ops/backup/dolibarr-backup.sh +++ b/ops/backup/dolibarr-backup.sh @@ -37,6 +37,29 @@ TMP_S3_SECRET="dolibarr-backup-s3-temp" log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } die() { printf '\033[1;31mABORT:\033[0m %s\n' "$*" >&2; exit 1; } +# --- garde de cluster ------------------------------------------------------- +# Ce script lit des secrets, crée des Jobs et peut RESTAURER une base. Lancé sur +# le mauvais contexte kubectl, il part sur l'infrastructure de quelqu'un d'autre. +# Le contexte courant d'une station de travail n'est pas une garantie : il suffit +# d'un `K config use-context` oublié. On l'épingle donc, et on vérifie une +# empreinte POSITIVE du homelab avant d'agir — mêmes garde-fous que +# ops/sandbox/sandbox-lifecycle.sh. +ERP_KUBE_CONTEXT="${ERP_KUBE_CONTEXT:-default}" +K() { kubectl --context "$ERP_KUBE_CONTEXT" "$@"; } + +assert_arcodange_cluster() { + kubectl config get-contexts -o name 2>/dev/null | grep -qx "$ERP_KUBE_CONTEXT" \ + || die "kube-context '$ERP_KUBE_CONTEXT' n'existe pas (définir ERP_KUBE_CONTEXT)" + for ns in erp erp-sandbox "$S3_SRC_NS"; do + K get ns "$ns" >/dev/null 2>&1 \ + || die "le contexte '$ERP_KUBE_CONTEXT' n'a pas de namespace '$ns' — refus de s'y exécuter. + Ce script lit des secrets et peut restaurer une base ; il ne doit viser que le homelab Arcodange. + Contexte courant : '$(kubectl config current-context 2>/dev/null)'. + Définir ERP_KUBE_CONTEXT sur le contexte du homelab et réessayer." + done + log "garde de cluster OK — contexte '$ERP_KUBE_CONTEXT' (namespaces erp/erp-sandbox/$S3_SRC_NS)" +} + CMD="${1:-}"; shift || true ENV="prod"; KEY=""; KIND=""; YES=0 while [[ $# -gt 0 ]]; do @@ -72,11 +95,11 @@ SH copy_s3_secret() { command -v python3 >/dev/null || die "python3 required to copy the S3 secret without exposing it" - kubectl get secret "$S3_SRC_SECRET" -n "$S3_SRC_NS" -o json \ + K get secret "$S3_SRC_SECRET" -n "$S3_SRC_NS" -o json \ | python3 -c "import json,sys; d=json.load(sys.stdin); d['metadata']={'name':'$TMP_S3_SECRET','namespace':'$NS'}; d.pop('status',None); d['data']={k:d['data'][k] for k in ('AWS_ACCESS_KEY_ID','AWS_SECRET_ACCESS_KEY','AWS_ENDPOINTS')}; print(json.dumps(d))" \ - | kubectl apply -f - >/dev/null + | K apply -f - >/dev/null } -cleanup_secret() { kubectl delete secret "$TMP_S3_SECRET" -n "$NS" --ignore-not-found >/dev/null 2>&1 || true; } +cleanup_secret() { K delete secret "$TMP_S3_SECRET" -n "$NS" --ignore-not-found >/dev/null 2>&1 || true; } # b64-encode an in-container script (host vars already substituted by the caller) b64() { printf '%s' "$1" | base64 | tr -d '\n'; } @@ -87,8 +110,8 @@ run_backup() { copy_s3_secret log "Backup ${ENV}: DB=$DB PVC=$PVC -> s3://$BUCKET/$PREFIX/{db,docs}/" local B64; B64="$(b64 "$(cat "${SCRIPT_DIR}/../../chart/files/backup-job.sh")")" - kubectl delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true - kubectl apply -f - >/dev/null </dev/null 2>&1 || true + K apply -f - >/dev/null </dev/null 2>&1 \ - || die "backup Job did not complete — kubectl logs -n $NS job/dolibarr-backup" - kubectl logs -n "$NS" job/dolibarr-backup | sed 's/^/ /' - kubectl delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true + K wait --for=condition=complete job/dolibarr-backup -n "$NS" --timeout=300s >/dev/null 2>&1 \ + || die "backup Job did not complete — K logs -n $NS job/dolibarr-backup" + K logs -n "$NS" job/dolibarr-backup | sed 's/^/ /' + K delete job dolibarr-backup -n "$NS" --ignore-not-found >/dev/null 2>&1 || true cleanup_secret; trap - EXIT log "Backup complete." } @@ -135,8 +158,8 @@ echo "db/:"; S3 ls "s3://$BUCKET/$PREFIX/db/" || echo " (empty)" echo "docs/:"; S3 ls "s3://$BUCKET/$PREFIX/docs/" || echo " (empty)" EOF )" - kubectl delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true - kubectl apply -f - >/dev/null </dev/null 2>&1 || true + K apply -f - >/dev/null </dev/null 2>&1 || true - kubectl logs -n "$NS" job/dolibarr-backup-list 2>/dev/null | sed 's/^/ /' - kubectl delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true + K wait --for=condition=complete job/dolibarr-backup-list -n "$NS" --timeout=180s >/dev/null 2>&1 || true + K logs -n "$NS" job/dolibarr-backup-list 2>/dev/null | sed 's/^/ /' + K delete job dolibarr-backup-list -n "$NS" --ignore-not-found >/dev/null 2>&1 || true cleanup_secret; trap - EXIT } @@ -167,8 +190,8 @@ run_restore() { copy_s3_secret local DEPLOY="$NS" # release/instance name == namespace (erp / erp-sandbox) log "Restore ${KIND} on '${ENV}' from ${KEY} (scaling ${DEPLOY} to 0)" - kubectl scale deploy "$DEPLOY" -n "$NS" --replicas=0 >/dev/null 2>&1 || true - kubectl wait --for=delete pod -l app.kubernetes.io/instance="$NS" -n "$NS" --timeout=120s >/dev/null 2>&1 || true + K scale deploy "$DEPLOY" -n "$NS" --replicas=0 >/dev/null 2>&1 || true + K wait --for=delete pod -l app.kubernetes.io/instance="$NS" -n "$NS" --timeout=120s >/dev/null 2>&1 || true local SCRIPT VOLS="[]" MOUNTS="[]" if [[ "$KIND" == "db" ]]; then @@ -197,8 +220,8 @@ EOF )" fi local B64; B64="$(b64 "$SCRIPT")" - kubectl delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true - kubectl apply -f - >/dev/null </dev/null 2>&1 || true + K apply -f - >/dev/null </dev/null 2>&1; then - kubectl logs -n "$NS" job/dolibarr-restore 2>&1 | sed 's/^/ /' - kubectl scale deploy "$DEPLOY" -n "$NS" --replicas=1 >/dev/null 2>&1 || true + if ! K wait --for=condition=complete job/dolibarr-restore -n "$NS" --timeout=300s >/dev/null 2>&1; then + K logs -n "$NS" job/dolibarr-restore 2>&1 | sed 's/^/ /' + K scale deploy "$DEPLOY" -n "$NS" --replicas=1 >/dev/null 2>&1 || true die "restore Job did not complete" fi - kubectl logs -n "$NS" job/dolibarr-restore | sed 's/^/ /' - kubectl delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true + K logs -n "$NS" job/dolibarr-restore | sed 's/^/ /' + K delete job dolibarr-restore -n "$NS" --ignore-not-found >/dev/null 2>&1 || true log "Scaling ${DEPLOY} back to 1" - kubectl scale deploy "$DEPLOY" -n "$NS" --replicas=1 >/dev/null 2>&1 || true + K scale deploy "$DEPLOY" -n "$NS" --replicas=1 >/dev/null 2>&1 || true cleanup_secret; trap - EXIT log "Restore complete." } +assert_arcodange_cluster + case "$CMD" in backup) run_backup ;; list) run_list ;; diff --git a/ops/sandbox/sandbox-lifecycle.sh b/ops/sandbox/sandbox-lifecycle.sh index 7761d24..a04e779 100755 --- a/ops/sandbox/sandbox-lifecycle.sh +++ b/ops/sandbox/sandbox-lifecycle.sh @@ -173,9 +173,117 @@ sync_documents() { log "Documents synced. (For a one-shot logo only, scope the tar to mycompany/logos.)" } +# --- mode vierge ------------------------------------------------------------- +# ÉTAT : INCOMPLET. La purge fonctionne, l'instance ne se reconstruit PAS. +# +# Vérifié le 2026-08-14 : après DROP OWNED et retrait de install.lock, Dolibarr +# sert une page de login sur un schéma à ZÉRO table et l'API répond « Module Api +# must be enabled ». L'entrypoint de l'image NE LANCE PAS d'installation +# automatique — retirer le verrou ne suffit pas. +# +# CE QU'IL MANQUE : un « golden empty », c'est-à-dire un pg_dump d'une instance +# Dolibarr fraîchement installée — schéma + données de référence (dictionnaires, +# pays, plan comptable) — sans aucune donnée métier. `blank` restaurerait ce dump +# au lieu de laisser la base vide, exactement comme `refresh-from-prod` restaure +# le dump de production. Seule la source change. +# +# En l'état, `blank` laisse le bac à sable INUTILISABLE : ne l'employer que suivi +# de `refresh-from-prod`, ou une fois le golden empty constitué. +# +# Vide la base du bac à sable SANS rien restaurer, pour qu'un rejeu d'exercice +# reparte de zéro. `refresh-from-prod` réimporterait précisément les défauts que +# le rejeu doit corriger : reconstruire suppose donc une base vide, pas iso-prod. +# +# CE QUI REND L'OPÉRATION SÛRE : elle est ANNULABLE. `refresh-from-prod` restaure +# un bac à sable iso-prod en trois minutes. On peut donc casser sans regret — et +# c'est la seule raison pour laquelle une commande qui détruit une base entière +# est acceptable ici. +# +# Elle ne vise QUE $SB_NS. Le namespace de production n'apparaît nulle part dans +# cette fonction, et la garde de cluster refuse tout contexte qui n'est pas le +# homelab. +blank_sandbox() { + [ "${1:-}" = "--yes" ] || die "blank détruit toutes les données du bac à sable — relancer avec --yes" + trap restore_state EXIT + + log "Pause du self-heal ArgoCD (sinon le scale-to-0 est repris en quelques secondes)" + set_selfheal false + + log "erp-sandbox à 0 réplique (accès exclusif à la base)" + K scale deploy erp-sandbox -n "$SB_NS" --replicas=0 >/dev/null + K wait --for=delete pod -l app.kubernetes.io/instance=erp-sandbox -n "$SB_NS" --timeout=120s >/dev/null 2>&1 || true + + log "Job de purge (DROP OWNED, aucune restauration)" + K delete job sandbox-blank -n "$SB_NS" --ignore-not-found >/dev/null 2>&1 || true + K apply -f - >/dev/null </dev/null 2>&1 \ + || die "le Job de purge n'a pas abouti — voir : K logs -n $SB_NS job/sandbox-blank" + K logs -n "$SB_NS" job/sandbox-blank | sed 's/^/ /' + K delete job sandbox-blank -n "$SB_NS" --ignore-not-found >/dev/null 2>&1 || true + + # Le verrou d'installation vit sur le VOLUME DOCUMENTS, que la purge de la base + # ne touche pas. Tant qu'il est là, Dolibarr saute l'installeur et sert une page + # de login sur un schéma inexistant : l'instance a l'air vivante et ne l'est pas. + # Le retirer laisse l'entrypoint de l'image réinstaller un schéma neuf. + log "Retrait du verrou d'installation (sinon Dolibarr sert un login sur une base sans schéma)" + K scale deploy erp-sandbox -n "$SB_NS" --replicas=1 >/dev/null + K wait --for=condition=ready pod -l app.kubernetes.io/instance=erp-sandbox -n "$SB_NS" --timeout=180s >/dev/null 2>&1 || true + POD=$(sb_pod) + [ -n "$POD" ] && K exec -n "$SB_NS" "$POD" -- rm -f /var/www/documents/install.lock >/dev/null 2>&1 || true + + log "Redémarrage pour déclencher l'installation" + K rollout restart deploy erp-sandbox -n "$SB_NS" >/dev/null + K rollout status deploy erp-sandbox -n "$SB_NS" --timeout=300s >/dev/null 2>&1 || true + + log "Réarmement du self-heal ArgoCD" + set_selfheal true + + log "Base vidée et verrou retiré. Vérifier que le schéma se reconstruit :" + log " curl -s https://erp-sandbox.arcodange.lab/api/index.php/status" + log "Annulation à tout moment : $0 refresh-from-prod" +} + case "${1:-}" in refresh-from-prod) assert_arcodange_cluster; refresh_from_prod ;; + blank) assert_arcodange_cluster; shift; blank_sandbox "${1:-}" ;; sync-documents) assert_arcodange_cluster; sync_documents ;; refresh) assert_arcodange_cluster; refresh_from_prod; sync_documents ;; - *) echo "usage: $0 {refresh-from-prod|sync-documents|refresh}" >&2; exit 2 ;; + *) echo "usage: $0 {refresh-from-prod|sync-documents|refresh|blank --yes}" >&2; exit 2 ;; esac -- 2.54.0 From bc5e0603d1a726c53b7742baa18327029e708861 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Fri, 14 Aug 2026 19:13:57 +0200 Subject: [PATCH 4/6] =?UTF-8?q?docs(ops):=20ne=20jamais=20=C3=A9diter=20le?= =?UTF-8?q?=20script=20de=20cycle=20de=20vie=20pendant=20son=20ex=C3=A9cut?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bash lit ses scripts au fil de l'eau. Éditer le fichier en cours de route décale les offsets et corrompt l'exécution : un refresh-from-prod en cours s'est mis à afficher les messages de blank_sandbox, puis a fini sur « syntax error ». Le travail avait bien abouti — sandbox restaurée, 295 tables, 10 factures, 12 tiers — mais la trace mentait sur la fonction qui tournait. Sur un script qui purge des bases, une trace qui ment est inacceptable. Co-Authored-By: Claude Opus 5 (1M context) --- ops/sandbox/sandbox-lifecycle.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ops/sandbox/sandbox-lifecycle.sh b/ops/sandbox/sandbox-lifecycle.sh index a04e779..c4122f5 100755 --- a/ops/sandbox/sandbox-lifecycle.sh +++ b/ops/sandbox/sandbox-lifecycle.sh @@ -35,6 +35,14 @@ PG_IMAGE="postgres:16-alpine" DOC_ROOT="/var/www/documents" # dolibarr_main_data_root TMP_PROD_SECRET="prod-db-ro-temp" # transient copy of prod creds, deleted on exit +# ATTENTION — NE JAMAIS ÉDITER CE FICHIER PENDANT QU'IL S'EXÉCUTE. +# Bash lit ses scripts au fil de l'eau : modifier le fichier en cours de route +# décale les offsets et corrompt l'exécution. Constaté le 2026-08-14 — un +# refresh-from-prod en cours s'est mis à afficher les messages de blank_sandbox +# puis a fini sur « syntax error ». Le travail était fait, mais la trace mentait +# sur ce qui tournait : sur un script qui purge des bases, c'est inacceptable. +# Attendre la fin, ou éditer une copie. + log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; } die() { printf '\033[1;31mABORT:\033[0m %s\n' "$*" >&2; exit 1; } -- 2.54.0 From e175f1fead07a30c8cb0eb2741f3dfdd43c8b018 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Fri, 14 Aug 2026 19:32:17 +0200 Subject: [PATCH 5/6] =?UTF-8?q?fix(test):=20garde=20d'h=C3=B4te=20sur=20ma?= =?UTF-8?q?in.ts=20=E2=80=94=20le=20script=20qui=20pilote=20l'installeur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main.ts pilote l'ASSISTANT D'INSTALLATION de Dolibarr (initialSetup.ts va sur /install/). C'est le script le plus destructeur du dépôt, et il était le seul des scripts UI à n'avoir aucune garde d'hôte — alors que la docstring de guard.ts nomme précisément le danger : « test/.env ships DOLIBARR_ADDRESS pointing at PRODUCTION, so a script run with the ambient environment would drive the real ERP ». Pointé sur la production avec l'environnement ambiant, il relançait l'installeur sur l'ERP réel. assertSandbox() n'autorise désormais que le bac à sable, sauf double opt-in explicite (ARCO_ALLOW_PRODUCTION + ARCO_PROD_CONFIRM) — que l'installation initiale d'un nouvel environnement justifie, et qui reste un geste d'opérateur. Vérifié : DOLIBARR_ADDRESS=https://erp.arcodange.lab est refusé. Ceci clôt le mode vierge, qui n'avait rien à inventer. La chaîne existait déjà en entier : sandbox-lifecycle.sh blank --yes purge la base, retire install.lock test/main.ts pilote l'installeur, société, affichage, modules touch install.lock reverrouille (procédure du README) test/provisionSandbox.ts agent d'écriture, droits, clé API Le « golden empty » proposé au commit précédent était une invention pour un problème résolu : je n'avais pas regardé test/. C'est exactement le contrôle inscrit dans AGENTS.md le matin même — lire l'existant avant de modéliser. Co-Authored-By: Claude Opus 5 (1M context) --- test/main.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/main.ts b/test/main.ts index 5149f84..a745c3f 100644 --- a/test/main.ts +++ b/test/main.ts @@ -2,6 +2,7 @@ import "load_dotenv"; import { chromium } from "playwright"; import initialSetup from "./scripts/admin/initialSetup.ts"; import login from "./scripts/login.ts"; +import { assertSandbox } from "./scripts/guard.ts"; import companySetup, { Company } from "./scripts/admin/companySetup.ts"; import path from "node:path"; import displaySetup from "./scripts/admin/displaySetup.ts"; @@ -10,8 +11,14 @@ import moduleSetup from "./scripts/admin/moduleSetup.ts"; /* Initialisation */ -const dolibarrAddress = Deno.env.get("DOLIBARR_ADDRESS") || - "https://erp.arcodange.duckdns.org"; +// GARDE D'HÔTE. main.ts pilote l'ASSISTANT D'INSTALLATION (`/install/`) : c'est +// le script le plus destructeur du dépôt. Pointé sur la production avec +// l'environnement ambiant — et `test/.env` porte justement DOLIBARR_ADDRESS sur +// la PRODUCTION — il relancerait l'installeur sur l'ERP réel. +// assertSandbox() n'autorise que le bac à sable, sauf double opt-in explicite +// (ARCO_ALLOW_PRODUCTION + ARCO_PROD_CONFIRM) pour l'installation initiale d'un +// nouvel environnement, qui reste un geste d'opérateur. +const dolibarrAddress = assertSandbox(); const debug = true; const DBpassword = Deno.env.get("DOLI_DB_PASSWORD") || "undefined"; const adminCredentials = { -- 2.54.0 From 6a5b7baa23b8ac80eb3864fc3fc5bf63a3dfb1b5 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Fri, 14 Aug 2026 22:04:43 +0200 Subject: [PATCH 6/6] =?UTF-8?q?chore:=20r=C3=A9concilier=20le=20trunk=20av?= =?UTF-8?q?ec=20main,=20et=20corriger=20la=20note=20du=20mode=20vierge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEUX CHOSES. 1. RÉCONCILIATION. Le trunk accusait 49 commits de retard, 0 d'avance : tout son delta vivait dans l'arbre de travail, mélange d'améliorations réelles et d'état périmé. Il est remis sur origin/main, et son état est préservé sur la branche trunk-snapshot-20260814. Ce qui méritait d'être porté l'est ici : - test/main.ts : chemin Work/ au lieu de Desktop/ (l'ancien n'existe plus) et module TIERS activé. L'URL par défaut reste corrigée par la garde d'hôte ; - ansible README : la commande du playbook recurrentBackup ; - company.json : siteWeb arcodange.fr et email @arcodange.fr — le trunk était à jour sur ces deux champs quand main portait encore duckdns et gmail. NE SONT PAS PORTÉS, délibérément : - le .gitignore du trunk, qui avait PERDU les règles replay-packs ; - company.json SAS / 6201Z, périmé — main porte SARL / 6201Z depuis le PR #77 ; - un console.log de débogage dans forms.ts ; - createEphemeralAgent + permissions.ts + orchestratorExample.ts : travail cohérent mais qui DOUBLE le modèle de scopes durables (scopes.ts + provisionAiUser.ts) effectivement livré. Deux conceptions concurrentes de la même chose ne se portent pas en silence — arbitrage opérateur. 2. LA NOTE DU MODE VIERGE ÉTAIT FAUSSE. Elle annonçait qu'il manquait un « golden empty ». Rien ne manquait : la chaîne existe en entier dans test/, et c'est main.ts qui pilote l'installeur. J'avais proposé de construire ce qui était déjà là, faute d'avoir regardé — exactement le contrôle inscrit dans AGENTS.md le matin même. La note décrit désormais la chaîne réelle : blank --yes -> main.ts -> install.lock -> provisionSandbox.ts Co-Authored-By: Claude Opus 5 (1M context) --- ansible/arcodange/erp/README.md | 4 ++++ ops/sandbox/sandbox-lifecycle.sh | 28 +++++++++++----------------- static/config/company.json | 8 ++++---- test/main.ts | 4 ++-- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/ansible/arcodange/erp/README.md b/ansible/arcodange/erp/README.md index 289e04a..4830a4f 100644 --- a/ansible/arcodange/erp/README.md +++ b/ansible/arcodange/erp/README.md @@ -1,3 +1,7 @@ # Ansible Collection - arcodange.erp Documentation for the collection. + +```sh + ansible-playbook playbooks/recurrentBackup.yml -i ~/.ansible/collections/ansible_collections/arcodange/factory/inventory -vv + ``` \ No newline at end of file diff --git a/ops/sandbox/sandbox-lifecycle.sh b/ops/sandbox/sandbox-lifecycle.sh index c4122f5..a0112a2 100755 --- a/ops/sandbox/sandbox-lifecycle.sh +++ b/ops/sandbox/sandbox-lifecycle.sh @@ -182,25 +182,19 @@ sync_documents() { } # --- mode vierge ------------------------------------------------------------- -# ÉTAT : INCOMPLET. La purge fonctionne, l'instance ne se reconstruit PAS. +# Vide la base du bac à sable pour qu'un rejeu d'exercice reparte de zéro. +# `refresh-from-prod` réimporterait précisément les défauts que le rejeu doit +# corriger : reconstruire suppose donc une base vide, pas iso-prod. # -# Vérifié le 2026-08-14 : après DROP OWNED et retrait de install.lock, Dolibarr -# sert une page de login sur un schéma à ZÉRO table et l'API répond « Module Api -# must be enabled ». L'entrypoint de l'image NE LANCE PAS d'installation -# automatique — retirer le verrou ne suffit pas. +# CETTE COMMANDE N'EST QU'UNE ÉTAPE. Seule, elle laisse le bac à sable sans +# schéma — Dolibarr sert alors une page de login sur une base vide. La chaîne +# complète, déjà outillée dans test/ : # -# CE QU'IL MANQUE : un « golden empty », c'est-à-dire un pg_dump d'une instance -# Dolibarr fraîchement installée — schéma + données de référence (dictionnaires, -# pays, plan comptable) — sans aucune donnée métier. `blank` restaurerait ce dump -# au lieu de laisser la base vide, exactement comme `refresh-from-prod` restaure -# le dump de production. Seule la source change. -# -# En l'état, `blank` laisse le bac à sable INUTILISABLE : ne l'employer que suivi -# de `refresh-from-prod`, ou une fois le golden empty constitué. -# -# Vide la base du bac à sable SANS rien restaurer, pour qu'un rejeu d'exercice -# reparte de zéro. `refresh-from-prod` réimporterait précisément les défauts que -# le rejeu doit corriger : reconstruire suppose donc une base vide, pas iso-prod. +# 1. sandbox-lifecycle.sh blank --yes purge la base, retire install.lock +# 2. cd test && deno run -A main.ts pilote l'installeur (/install/), la +# société, l'affichage et les modules +# 3. touch install.lock dans le pod reverrouille (recette du test/README.md) +# 4. deno run -A provisionSandbox.ts agent d'écriture, droits, clé API # # CE QUI REND L'OPÉRATION SÛRE : elle est ANNULABLE. `refresh-from-prod` restaure # un bac à sable iso-prod en trois minutes. On peut donc casser sans regret — et diff --git a/static/config/company.json b/static/config/company.json index 253fb1b..d78af01 100644 --- a/static/config/company.json +++ b/static/config/company.json @@ -6,8 +6,8 @@ "ville": "Evry Courcouronnes", "pays": "France (FR)", "telephonePortable": "0664574539", - "siteWeb": "gitea.arcodange.duckdns.org", - "email": "arcodange@gmail.com", + "siteWeb": "arcodange.fr", + "email": "gabrielradureau@arcodange.fr", "logoFilePath": "$IMG/logo512.png" }, "ID": { @@ -22,5 +22,5 @@ "numTva": "FR00999657455", "rcs_rm": "R.C.S. Évry", "moisDebutExercice": "Janvier" - } -} \ No newline at end of file + } +} diff --git a/test/main.ts b/test/main.ts index a745c3f..7c22e2d 100644 --- a/test/main.ts +++ b/test/main.ts @@ -27,7 +27,7 @@ const adminCredentials = { }; const rootFolderPath = Deno.env.get("ROOT_FOLDER") || - "/Users/gabrielradureau/Desktop/ARCODANGE/erp"; + "/Users/gabrielradureau/Work/Arcodange/erp"; const imgFolderPath = Deno.env.get("IMG_FOLDER") || path.join(rootFolderPath, "static/img"); const configFolderPath = Deno.env.get("CONFIG_FOLDER") || @@ -78,7 +78,7 @@ const globalCtx = { try { // await displaySetup.setupDisplay(globalCtx); - await moduleSetup.configureModule(globalCtx, {moduleName: 'TIERS' ,enabled: false, onConfiguration: async () => { + await moduleSetup.configureModule(globalCtx, {moduleName: 'TIERS' , enabled: true, onConfiguration: async () => { await globalCtx.page.screenshot({path: 'save.png'}) }}); } catch (error) { -- 2.54.0