chore(write-skill): preserve the 2026-07-11 books-regularization replay pack
The pack (manifests, prelude, runbook, verify-provenance.py PoC) lived only in an ephemeral session scratchpad while erp#41/#42/#43/#44 now reference it as fixtures and the prod replay is still pending. 36/36 provenance checks were green at rehearsal time; PDFs are re-fetchable via arcodange-email-ingest (documented in the pack README). Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Replay pack — books regularization 2026-07-11
|
||||||
|
|
||||||
|
The complete, provenance-verified write pack from the 2026-07-11 books-regularization session
|
||||||
|
(memory: `books-gap-2026-07`). Rehearsed green on the sandbox (FAF2026010–013 + payments,
|
||||||
|
predicted-delta verified). **Prod replay: pending the human gate** — status lives in the
|
||||||
|
session journal / operator's hands, not in this folder.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
| File | What it is |
|
||||||
|
| --- | --- |
|
||||||
|
| `manifest-A-km-payments.json` | 2 KissMetrics customer payments (Wise wires 2 147,00 € + 2 195,97 €, tx ids from the activity feed) |
|
||||||
|
| `manifest-B-suppliers.json` | DARNIS F1045 supplier invoice (214,70 HT / 257,64 TTC) + its payment + Anthropic/Mistral payment ops |
|
||||||
|
| `manifest-B2-sandbox-payments.json` | Sandbox-side payment re-run after the varchar(50) fix (erp#37) |
|
||||||
|
| `prod-replay-prelude.sh` | Pre-replay guards: fresh checkpoint assumptions, target checks, env pinning |
|
||||||
|
| `rehearsal-runbook.md` | Step-by-step of the rehearsal: what ran, in what order, with which gates |
|
||||||
|
| `verify-provenance.py` | **The anti-hallucination PoC** — 36 field-level checks: every critical value re-verified against source-PDF text (pdftotext) + FRESH Qonto/Wise pulls; locale-normalized (`219,50`≡`219.50`, Wise `2,147` thousands format). 36/36 green at rehearsal time. |
|
||||||
|
|
||||||
|
## Source PDFs (not committed — re-fetchable)
|
||||||
|
|
||||||
|
`verify-provenance.py` expects `./pdfs/` containing:
|
||||||
|
`F1045_ARCODANGE_2026-06-30.pdf`, `F1046_ARCODANGE_2026-06-29.pdf`,
|
||||||
|
`Invoice-9BF0758D-695749.pdf` (Anthropic), `invoice-MSTRL-API-814045-001.pdf` (Mistral).
|
||||||
|
Re-fetch via the `arcodange-email-ingest` skill (they live in `books@` — workflow 2 downloads
|
||||||
|
attachments by message id) or from the GED once erp#43 attaches them.
|
||||||
|
|
||||||
|
## Who consumes this pack
|
||||||
|
|
||||||
|
- **erp#41** (provenance checker as a promote-plan stage) — industrializes `verify-provenance.py`.
|
||||||
|
- **erp#42** (compliance linter) — these manifests are the *legitimate* fixtures that must PASS.
|
||||||
|
- **erp#44** (idempotency keys) — replay these manifests twice on a checkpoint; run 2 must be all-deduped.
|
||||||
|
- **erp#43** (GED attach) — attaches the four PDFs to FAF2026010–013 on the sandbox.
|
||||||
|
|
||||||
|
## Rule this pack proves
|
||||||
|
|
||||||
|
No agent-typed value reaches a write without **mechanical corroboration**: source-anchored
|
||||||
|
(the exact string exists in the source document) + cross-system (bank feed to the cent, fresh
|
||||||
|
pull at check time) + read-back (predicted-delta after apply). Refuse, never repair.
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"ref": "pay_km_may",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 14,
|
||||||
|
"kind": "customer",
|
||||||
|
"mode": "VIR",
|
||||||
|
"account_id": 2,
|
||||||
|
"date": "2026-05-29",
|
||||||
|
"transaction_id": "2159468139",
|
||||||
|
"comment": "Virement KM 2026-05-29 +2147.00 EUR (retainer USD, change au reglement; ecart +1.08 constate a part)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"ref": "pay_km_jun",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 16,
|
||||||
|
"kind": "customer",
|
||||||
|
"mode": "VIR",
|
||||||
|
"account_id": 2,
|
||||||
|
"date": "2026-06-25",
|
||||||
|
"transaction_id": "2210434850",
|
||||||
|
"comment": "Virement KM 2026-06-25 +2195.97 EUR (retainer USD, change au reglement; ecart +50.05 constate a part)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "invoice",
|
||||||
|
"ref": "darnis_jun2",
|
||||||
|
"input": {
|
||||||
|
"socid": "#thirdparty:name=Darnis Operations",
|
||||||
|
"kind": "supplier",
|
||||||
|
"date": "2026-06-30",
|
||||||
|
"ref_supplier": "F1045",
|
||||||
|
"validate": true,
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"desc": "Apport d'affaire Arcodange - Projet — juin 2026 (facture Hiway F1045 du 30/06/2026)",
|
||||||
|
"qty": 1,
|
||||||
|
"price_ht": 214.70,
|
||||||
|
"tva": 20,
|
||||||
|
"type": "service"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": "@darnis_jun2",
|
||||||
|
"kind": "supplier",
|
||||||
|
"mode": "VIR",
|
||||||
|
"account_id": 1,
|
||||||
|
"date": "2026-06-29",
|
||||||
|
"amount": 257.64,
|
||||||
|
"transaction_id": "019f14c5-e254-7ac9-9e9f-307ed9-d55f44",
|
||||||
|
"comment": "Qonto transfer 2026-06-29 DARNIS OPERATIONS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 12,
|
||||||
|
"kind": "supplier",
|
||||||
|
"mode": "VIR",
|
||||||
|
"account_id": 1,
|
||||||
|
"date": "2026-06-29",
|
||||||
|
"amount": 263.40,
|
||||||
|
"transaction_id": "019f14c6-db08-7e2a-8f9b-345d22-8db1ce",
|
||||||
|
"comment": "Qonto transfer 2026-06-29 DARNIS OPERATIONS — paie (PROV12) une fois validee"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 15,
|
||||||
|
"kind": "supplier",
|
||||||
|
"mode": "CB",
|
||||||
|
"account_id": 1,
|
||||||
|
"date": "2026-04-03",
|
||||||
|
"amount": 172.68,
|
||||||
|
"transaction_id": "019d4eb3-ca44-7e2b-a740-1abb49-2d9179",
|
||||||
|
"comment": "Mistral AI abonnement avril (carte Qonto 2026-04-03) — paie (PROV15) fournisseur une fois validee"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 14,
|
||||||
|
"kind": "supplier",
|
||||||
|
"mode": "CB",
|
||||||
|
"account_id": 1,
|
||||||
|
"date": "2026-04-13",
|
||||||
|
"amount": 180.00,
|
||||||
|
"transaction_id": "019d8287-427d-795b-89be-fe3582-0eb097",
|
||||||
|
"comment": "Anthropic abonnement avril (carte Qonto 2026-04-13) — paie (PROV14) fournisseur une fois validee"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 29,
|
||||||
|
"kind": "supplier",
|
||||||
|
"mode": "VIR",
|
||||||
|
"account_id": 1,
|
||||||
|
"date": "2026-06-29",
|
||||||
|
"amount": 257.64,
|
||||||
|
"transaction_id": "019f14c5-e254-7ac9-9e9f-307ed9-d55f44",
|
||||||
|
"comment": "Qonto transfer 2026-06-29 DARNIS OPERATIONS"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 12,
|
||||||
|
"kind": "supplier",
|
||||||
|
"mode": "VIR",
|
||||||
|
"account_id": 1,
|
||||||
|
"date": "2026-06-29",
|
||||||
|
"amount": 263.4,
|
||||||
|
"transaction_id": "019f14c6-db08-7e2a-8f9b-345d22-8db1ce",
|
||||||
|
"comment": "Qonto transfer 2026-06-29 DARNIS OPERATIONS — paie (PROV12) une fois validee"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 15,
|
||||||
|
"kind": "supplier",
|
||||||
|
"mode": "CB",
|
||||||
|
"account_id": 1,
|
||||||
|
"date": "2026-04-03",
|
||||||
|
"amount": 172.68,
|
||||||
|
"transaction_id": "019d4eb3-ca44-7e2b-a740-1abb49-2d9179",
|
||||||
|
"comment": "Mistral AI abonnement avril (carte Qonto 2026-04-03) — paie (PROV15) fournisseur une fois validee"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "payment",
|
||||||
|
"input": {
|
||||||
|
"invoice_id": 14,
|
||||||
|
"kind": "supplier",
|
||||||
|
"mode": "CB",
|
||||||
|
"account_id": 1,
|
||||||
|
"date": "2026-04-13",
|
||||||
|
"amount": 180.0,
|
||||||
|
"transaction_id": "019d8287-427d-795b-89be-fe3582-0eb097",
|
||||||
|
"comment": "Anthropic abonnement avril (carte Qonto 2026-04-13) — paie (PROV14) fournisseur une fois validee"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Prélude du replay PROD — reproduit les étapes hors-manifeste répétées avec
|
||||||
|
# succès sur la sandbox le 2026-07-11 (fiches, lignes, refs/dates, validations).
|
||||||
|
# À exécuter PAR L'HUMAIN, clé prod en main, AVANT les deux promote apply :
|
||||||
|
#
|
||||||
|
# export DOLIBARR_PROD_WRITE_KEY=... # jamais stockée
|
||||||
|
# export ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD
|
||||||
|
# bash prod-replay-prelude.sh
|
||||||
|
# bin/arcodange promote apply manifest-A-km-payments.json --target prod
|
||||||
|
# bin/arcodange promote apply manifest-B-suppliers.json --target prod
|
||||||
|
#
|
||||||
|
# Chaque étape relit l'objet après écriture. S'arrête au premier écart.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ERP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# Adapter si le script n'est pas copié dans le repo erp :
|
||||||
|
DPW="${DPW:-$HOME/Work/Arcodange/erp/.claude/skills/dolibarr-sandbox-write/scripts/dol-prod-write.sh}"
|
||||||
|
[ -x "$DPW" ] || { echo "dol-prod-write.sh introuvable ($DPW)"; exit 1; }
|
||||||
|
|
||||||
|
jqv() { jq -r "$1" ; }
|
||||||
|
step() { echo; echo "=== $* ==="; }
|
||||||
|
|
||||||
|
step "0. Pré-vols : les 7 pièces prod dans l'état attendu"
|
||||||
|
for i in 14 15 16 17; do "$DPW" GET "/invoices/$i" | jq -e '(.statut=="0")' >/dev/null \
|
||||||
|
|| { echo "invoice $i n'est pas en brouillon — STOP (état prod ≠ répétition)"; exit 1; }; done
|
||||||
|
for i in 12 14 15; do "$DPW" GET "/supplierinvoices/$i" | jq -e '(.statut=="0")' >/dev/null \
|
||||||
|
|| { echo "supplierinvoice $i n'est pas en brouillon — STOP"; exit 1; }; done
|
||||||
|
echo "OK — 7 brouillons présents"
|
||||||
|
|
||||||
|
step "1. Fiches fournisseur : Anthropic (11) + Mistral (12)"
|
||||||
|
"$DPW" PUT /thirdparties/11 '{"fournisseur":1,"code_fournisseur":"-1"}' >/dev/null
|
||||||
|
"$DPW" PUT /thirdparties/12 '{"fournisseur":1,"code_fournisseur":"-1"}' >/dev/null
|
||||||
|
for i in 11 12; do "$DPW" GET "/thirdparties/$i" | jq -r '[.id,.name,.fournisseur,.code_fournisseur]|@tsv'; done
|
||||||
|
|
||||||
|
step "2. Brouillons fournisseurs : refs, dates, lignes manquantes, paye fantôme"
|
||||||
|
EP_ANT=$(python3 -c "import datetime;print(int(datetime.datetime(2026,4,12).timestamp()))")
|
||||||
|
EP_MIS=$(python3 -c "import datetime;print(int(datetime.datetime(2026,4,2).timestamp()))")
|
||||||
|
"$DPW" PUT /supplierinvoices/12 '{"ref_supplier":"F1046"}' >/dev/null
|
||||||
|
"$DPW" PUT /supplierinvoices/14 "{\"ref_supplier\":\"9BF0758D-695749\",\"date\":${EP_ANT}}" >/dev/null
|
||||||
|
"$DPW" PUT /supplierinvoices/15 "{\"ref_supplier\":\"MSTRL-API-814045-001\",\"date\":${EP_MIS}}" >/dev/null
|
||||||
|
# lignes absentes sur 12 et 14 (constaté identique en sandbox ; re-vérifié ici)
|
||||||
|
L12=$("$DPW" GET /supplierinvoices/12 | jq '.lines|length')
|
||||||
|
L14=$("$DPW" GET /supplierinvoices/14 | jq '.lines|length')
|
||||||
|
[ "$L12" = "0" ] && "$DPW" POST /supplierinvoices/12/lines '{"description":"Apport d affaire Arcodange - Projet — juin 2026 (Hiway F1046)","pu_ht":"219.50","qty":"1","tva_tx":"20","product_type":"1"}' >/dev/null
|
||||||
|
[ "$L14" = "0" ] && "$DPW" POST /supplierinvoices/14/lines '{"description":"Claude Pro — abonnement avril 2026 (facture 9BF0758D-695749, autoliquidation UE)","pu_ht":"180.00","qty":"1","tva_tx":"0","product_type":"1"}' >/dev/null
|
||||||
|
"$DPW" PUT /supplierinvoices/14 '{"paye":0}' >/dev/null
|
||||||
|
"$DPW" PUT /supplierinvoices/15 '{"paye":0}' >/dev/null
|
||||||
|
for i in 12 14 15; do "$DPW" GET "/supplierinvoices/$i" | jq -r '[.id,.ref_supplier,(.lines|length),.total_ttc,.paye]|@tsv'; done
|
||||||
|
# garde-fou totaux (l'ajout de ligne ne doit PAS doubler)
|
||||||
|
"$DPW" GET /supplierinvoices/12 | jq -e '.total_ttc=="263.40000000"' >/dev/null || { echo "total 12 inattendu — STOP"; exit 1; }
|
||||||
|
"$DPW" GET /supplierinvoices/14 | jq -e '.total_ttc=="180.00000000"' >/dev/null || { echo "total 14 inattendu — STOP"; exit 1; }
|
||||||
|
|
||||||
|
step "3. Validation clients — ordre chronologique (mai avant juin)"
|
||||||
|
for i in 14 15 16 17; do "$DPW" POST "/invoices/$i/validate" '{"idwarehouse":0,"notrigger":0}' >/dev/null; done
|
||||||
|
for i in 14 15 16 17; do "$DPW" GET "/invoices/$i" | jq -r '[.id,.ref,.statut,.total_ttc]|@tsv'; done
|
||||||
|
|
||||||
|
step "4. Validation fournisseurs"
|
||||||
|
for i in 12 14 15; do "$DPW" POST "/supplierinvoices/$i/validate" '{"idwarehouse":0,"notrigger":0}' >/dev/null; done
|
||||||
|
for i in 12 14 15; do "$DPW" GET "/supplierinvoices/$i" | jq -r '[.id,.ref,.ref_supplier,.statut]|@tsv'; done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Prélude OK — enchaîner :"
|
||||||
|
echo " bin/arcodange promote apply manifest-A-km-payments.json --target prod"
|
||||||
|
echo " bin/arcodange promote apply manifest-B-suppliers.json --target prod"
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
# Régularisation des livres — runbook (état au 2026-07-11 soir)
|
||||||
|
|
||||||
|
**Où on en est : la répétition sandbox est FAITE et vérifiée.** Il reste le replay
|
||||||
|
prod (toi, 3 commandes), le bucket C (UI), et le contrôle final (moi).
|
||||||
|
|
||||||
|
## ✅ Fait — répétition sandbox complète (2026-07-11)
|
||||||
|
|
||||||
|
| Pièce | Avant | Après (sandbox) | Règlement |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| (PROV14) client mai | brouillon 2 145,92 | **FAC004-CL0001004** soldée | Wise `2159468139` @29/05 |
|
||||||
|
| (PROV15) client mai | brouillon 2 575,11 | **FAC005** validée, **due par KM** | — (cycle différé) |
|
||||||
|
| (PROV16) client juin | brouillon 2 145,92 | **FAC006** soldée | Wise `2210434850` @25/06 |
|
||||||
|
| (PROV17) client juin | brouillon 2 575,11 | **FAC007** validée, **due par KM** | — |
|
||||||
|
| (PROV15) fourn. Mistral | brouillon sans ref | **FAF2026010** / MSTRL-API-814045-001, payée | CB Qonto @03/04 |
|
||||||
|
| (PROV12) fourn. Darnis | brouillon sans ligne | **FAF2026011** / F1046, payée | VIR Qonto @29/06 |
|
||||||
|
| (PROV14) fourn. Anthropic | brouillon sans ligne | **FAF2026012** / 9BF0758D-695749, payée | CB Qonto @13/04 |
|
||||||
|
| — (manquante) | rien | **FAF2026013** / F1045 créée+validée, payée | VIR Qonto @29/06 |
|
||||||
|
|
||||||
|
Fiches corrigées : Anthropic (11) et Mistral (12) → fournisseur=1, codes FO0010/FO0011.
|
||||||
|
Agent sandbox : armé (droit 251 accordé — PR de pérennisation en cours par sous-agent).
|
||||||
|
|
||||||
|
**Appris pendant la répétition (intégré au prélude prod) :**
|
||||||
|
1. Deux brouillons fournisseurs n'avaient **aucune ligne** (totaux d'en-tête seulement) → lignes ajoutées depuis les PDF avant validation.
|
||||||
|
2. Les ids Qonto (67 c) dépassent `num_payment` varchar(50) → **forme canonique courte = suffixe UUID** (38 c) ; PR de normalisation bank-match/payment-record en cours par sous-agent.
|
||||||
|
3. Flag `paye=1` fantôme sur 2 brouillons → remis à 0 avant règlement.
|
||||||
|
4. `invoice-create` n'est pas idempotent (le re-run d'un manifeste dupliquerait) → ne jamais rejouer un manifeste partiellement appliqué ; pour prod, tout part de zéro donc manifeste B complet OK.
|
||||||
|
|
||||||
|
## ➡️ À toi — replay prod (10 min, ta clé, jamais stockée)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ~/Work/Arcodange/erp
|
||||||
|
export DOLIBARR_PROD_WRITE_KEY=... # ta clé d'écriture prod
|
||||||
|
export ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD
|
||||||
|
bash <scratchpad>/prod-replay-prelude.sh # fiches+lignes+refs+validations, avec pré-vols et garde-fous
|
||||||
|
bin/arcodange promote plan <scratchpad>/manifest-A-km-payments.json # relire
|
||||||
|
bin/arcodange promote apply <scratchpad>/manifest-A-km-payments.json --target prod
|
||||||
|
bin/arcodange promote plan <scratchpad>/manifest-B-suppliers.json
|
||||||
|
bin/arcodange promote apply <scratchpad>/manifest-B-suppliers.json --target prod
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prélude s'arrête au premier écart (il vérifie l'état prod avant chaque geste,
|
||||||
|
et que l'ajout de lignes ne double pas les totaux). Ordre intangible :
|
||||||
|
prélude (validations) AVANT les manifestes (règlements).
|
||||||
|
|
||||||
|
## ➡️ À toi — bucket C (UI Dolibarr, hors manifeste)
|
||||||
|
|
||||||
|
Apport en capital **1 000 €** (21/01 → 1013) · URSSAF **493,00 €** (22/05 → 6451,
|
||||||
|
module Charges sociales) · écarts de change **+1,08 et +50,05** (766, sur FAC004/FAC006) ·
|
||||||
|
cashback Wise **+1,60 €** (03/06) · frais/rembt Qonto **+5,22 €** (16/01).
|
||||||
|
Pièces à clarifier : facture **Cloudflare** (mail 24/06) ; reçus **Anthropic mai/juin**
|
||||||
|
(#2344, #2997 — pas de débit Qonto → carte perso ? les PDFs sont dans books@).
|
||||||
|
|
||||||
|
## ➡️ À moi — contrôle final (dès ton replay fait)
|
||||||
|
|
||||||
|
`bank match --since 2026-01-01` → cible : bucket UNKNOWN = 0 ; re-audit T15 ;
|
||||||
|
mise à jour de la mémoire `books-gap-2026-07`. Rappel : **facture KM juillet ~23/07**
|
||||||
|
(template toujours OFF — décision D3 du PRD).
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Provenance check — every critical value in the write pack must be
|
||||||
|
mechanically corroborated by (a) the source PDF text and/or (b) a FRESH
|
||||||
|
bank-feed pull. No value is trusted because an LLM typed it.
|
||||||
|
Exit 0 = all green; any FAIL = exit 1."""
|
||||||
|
import json, re, subprocess, sys, os
|
||||||
|
|
||||||
|
SCRATCH = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
PDF = os.path.join(SCRATCH, "pdfs")
|
||||||
|
ERP = os.path.expanduser("~/Work/Arcodange/erp")
|
||||||
|
BC = os.path.join(ERP, ".claude/skills/arcodange-bank-reco/scripts/bank-curl.sh")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
def check(name, ok, detail=""):
|
||||||
|
results.append((name, ok, detail))
|
||||||
|
|
||||||
|
def pdftext(fname):
|
||||||
|
p = subprocess.run(["pdftotext", "-layout", os.path.join(PDF, fname), "-"],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
return p.stdout
|
||||||
|
|
||||||
|
def norm(t): # 1 234,56 / 1,234.56 / 219.50 -> comparable tokens
|
||||||
|
return t.replace(" ", " ").replace(",", ".")
|
||||||
|
|
||||||
|
def has_amount(text, amount):
|
||||||
|
# match 219.50 or 219,50, tolerant to thousand separators/spacing
|
||||||
|
a = f"{amount:.2f}"
|
||||||
|
pat = a.replace(".", r"[.,]")
|
||||||
|
return re.search(pat, norm(text)) is not None
|
||||||
|
|
||||||
|
# ---------- (a) PDF-anchored facts ----------
|
||||||
|
DOCS = {
|
||||||
|
"F1045_ARCODANGE_2026-06-30.pdf": {
|
||||||
|
"ref": "F1045", "date_str": "30/06/2026",
|
||||||
|
"amounts": [214.70, 42.94, 257.64],
|
||||||
|
"line_hint": "Apport d'affaire"},
|
||||||
|
"F1046_ARCODANGE_2026-06-29.pdf": {
|
||||||
|
"ref": "F1046", "date_str": "29/06/2026",
|
||||||
|
"amounts": [219.50, 43.90, 263.40],
|
||||||
|
"line_hint": "Apport d'affaire"},
|
||||||
|
"Invoice-9BF0758D-695749.pdf": {
|
||||||
|
"ref": "9BF0758D-695749", "date_str": "April 12, 2026",
|
||||||
|
"amounts": [180.00],
|
||||||
|
"line_hint": "Claude Pro"},
|
||||||
|
"invoice-MSTRL-API-814045-001.pdf": {
|
||||||
|
"ref": "MSTRL-API-814045-001", "date_str": "2 avr. 2026",
|
||||||
|
"amounts": [143.90, 28.78, 172.68],
|
||||||
|
"line_hint": "Le Chat Pro"},
|
||||||
|
}
|
||||||
|
for fname, exp in DOCS.items():
|
||||||
|
t = pdftext(fname)
|
||||||
|
if not t.strip():
|
||||||
|
check(f"{fname}: text layer", False, "empty pdftotext"); continue
|
||||||
|
check(f"{fname}: ref {exp['ref']}", exp["ref"] in t)
|
||||||
|
check(f"{fname}: date {exp['date_str']}", exp["date_str"] in t)
|
||||||
|
for a in exp["amounts"]:
|
||||||
|
check(f"{fname}: amount {a:.2f}", has_amount(t, a))
|
||||||
|
check(f"{fname}: line '{exp['line_hint']}'", exp["line_hint"] in t)
|
||||||
|
|
||||||
|
# ---------- (b) fresh bank-feed corroboration ----------
|
||||||
|
def bank(pathsvc, path):
|
||||||
|
p = subprocess.run([BC, pathsvc, path], capture_output=True, text=True)
|
||||||
|
return json.loads(p.stdout) if p.returncode == 0 and p.stdout.strip() else {}
|
||||||
|
|
||||||
|
org = bank("qonto", "/v2/organization")
|
||||||
|
acct = next(a["id"] for a in org["organization"]["bank_accounts"] if a["status"] == "active")
|
||||||
|
qtx = []
|
||||||
|
for window in [("2026-04-01", "2026-04-15"), ("2026-06-28", "2026-06-30")]:
|
||||||
|
d = bank("qonto", f"/v2/transactions?bank_account_id={acct}&settled_at_from={window[0]}T00:00:00Z&settled_at_to={window[1]}T23:59:59Z&per_page=100¤t_page=1")
|
||||||
|
qtx += d.get("transactions", [])
|
||||||
|
qonto_feed = {(t["transaction_id"].split("transaction-")[-1]): (float(t["amount"]), t["side"]) for t in qtx}
|
||||||
|
|
||||||
|
envf = os.path.join(ERP, ".claude/skills/dolibarr/.env")
|
||||||
|
wpid = ""
|
||||||
|
for line in open(envf):
|
||||||
|
if line.startswith("WISE_PROFILE_ID"):
|
||||||
|
wpid = line.split("=", 1)[1].strip().strip('"')
|
||||||
|
wact = bank("wise", f"/v1/profiles/{wpid}/activities?since=2026-05-25T00:00:00.000Z&until=2026-06-30T23:59:59.000Z")
|
||||||
|
wise_feed = {}
|
||||||
|
for a in wact.get("activities", []):
|
||||||
|
if a.get("type") == "TRANSFER":
|
||||||
|
m = re.search(r"([\d,]+(?:\.\d{1,2})?)", a.get("primaryAmount", ""))
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
amt = float(m.group(1).replace(",", "")) # "2,195.97"->2195.97 ; "2,147"->2147.0 (comma = thousands sep)
|
||||||
|
wise_feed[str((a.get("resource") or {}).get("id", ""))] = amt
|
||||||
|
|
||||||
|
manA = json.load(open(os.path.join(SCRATCH, "manifest-A-km-payments.json")))
|
||||||
|
manB = json.load(open(os.path.join(SCRATCH, "manifest-B-suppliers.json")))
|
||||||
|
EXPECT_A = {"2159468139": 2147.00, "2210434850": 2195.97} # full wire amounts (invoice pays remaining 2145.92; delta = FX, booked separately)
|
||||||
|
for op in manA:
|
||||||
|
tx = op["input"]["transaction_id"]
|
||||||
|
check(f"manifest A: Wise tx {tx} exists in fresh feed", tx in wise_feed,
|
||||||
|
f"feed={wise_feed.get(tx)}")
|
||||||
|
if tx in wise_feed:
|
||||||
|
check(f"manifest A: Wise tx {tx} amount == feed", abs(wise_feed[tx] - EXPECT_A[tx]) < 0.005,
|
||||||
|
f"feed {wise_feed[tx]} vs expected wire {EXPECT_A[tx]}")
|
||||||
|
for op in manB:
|
||||||
|
if op["op"] != "payment":
|
||||||
|
# invoice op: amounts corroborated by the F1045 PDF above
|
||||||
|
li = op["input"]["lines"][0]
|
||||||
|
check("manifest B: F1045 line HT in PDF", has_amount(pdftext("F1045_ARCODANGE_2026-06-30.pdf"), float(li["price_ht"])))
|
||||||
|
check("manifest B: F1045 ref_supplier matches PDF ref", op["input"]["ref_supplier"] == "F1045")
|
||||||
|
continue
|
||||||
|
tx = op["input"]["transaction_id"]; amt = float(op["input"]["amount"])
|
||||||
|
check(f"manifest B: Qonto tx …{tx[-12:]} exists in fresh feed", tx in qonto_feed, f"known={list(qonto_feed)[:2]}…")
|
||||||
|
if tx in qonto_feed:
|
||||||
|
famt, side = qonto_feed[tx]
|
||||||
|
check(f"manifest B: tx …{tx[-12:]} amount {amt:.2f} == feed", abs(famt - amt) < 0.005 and side == "debit",
|
||||||
|
f"feed {famt} {side}")
|
||||||
|
|
||||||
|
# ---------- report ----------
|
||||||
|
w = max(len(n) for n, _, _ in results)
|
||||||
|
fails = 0
|
||||||
|
for n, ok, det in results:
|
||||||
|
print(f"{'PASS' if ok else 'FAIL':4} {n:<{w}} {det if not ok else ''}")
|
||||||
|
fails += 0 if ok else 1
|
||||||
|
print(f"\n{len(results)-fails}/{len(results)} checks green")
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
Reference in New Issue
Block a user