Commit Graph
17 Commits
Author SHA1 Message Date
arcodangeandClaude Opus 5 81cc3df13c feat(erp): enregistrer les charges sociales — script + runbook rationalisés
Dolibarr n'expose AUCUNE API REST pour les charges sociales (/taxes,
/socialcontributions, /chargesociales répondent tous « API not found »). Le
module est actif, seule l'API manque : le pipeline de promotion, qui parle REST,
ne peut pas porter cette opération. D'où un script UI, gardé par guard.ts.

La sandbox a joué son rôle : quatre doublons y ont été créés pendant la
découverte, sans conséquence, et les quatre pièges du formulaire sont désormais
documentés au lieu d'être redécouverts.

- recordSocialCharge.ts : idempotent (cherche la charge avant de créer),
  vérifie par LECTURE de la liste, type TNS par défaut, --dry-run.
- RUNBOOK_charges_sociales.md : écrit pour être suivi par un agent moins
  performant ou un harness limité — la commande, les garanties, les quatre
  pièges, et ce que le script ne garantit PAS (ni juge, ni artefact de gate).

Les quatre pièges, tous rencontrés :
1. La date visible est décorative : le backend ne lit que les champs CACHÉS
   echday/echmonth/echyear. Remplir le champ texte crée l'enregistrement avec
   une période aberrante (20/06/2000 observé) au lieu d'échouer.
2. Le bouton de soumission n'a pas d'attribut name — le cibler par value.
3. L'URL après soumission ne porte pas d'id : vérifier par l'URL fait conclure
   à un échec sur une création réussie. C'est ce qui a produit les doublons.
4. Les milliers portent une espace insécable (« 1 215,00 ») : une comparaison
   littérale casse au-delà de 999 € et l'idempotence saute en silence.

Correction comptable : 645x → 646 dans known-patterns.json. Les cotisations d'un
gérant TNS sont des cotisations personnelles du dirigeant (646), pas des
cotisations patronales sur salaires (645) — Arcodange n'a aucun salarié.

Appliqué en production : les trois échéances URSSAF 2026, toutes IMPAYÉES.
Reste à vérifier dans le dictionnaire Dolibarr que le type « indépendants »
porte bien le code comptable 646.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-08-13 17:27:17 +02:00
arcodangeandClaude Opus 5 dbe4b36c62 feat(write-skill): chronology guard + explicit production opt-in
Two guards, both from real incidents in the same session.

1. invoice-create.sh — chronology (CGI art. 289). Dolibarr assigns the number at
   validation, in creation order, so issuing a document dated BEFORE the last one
   already issued gives a higher number to an earlier date. The July plan walked
   straight into it: the M3 deferred part is due 2026-10-23 and must be issued at
   D-60 (24/08) to stay under the L.441-10 I ceiling, while the M4 fixed part is
   dated 23/08 — issue them in the wrong order and the numbering breaks. The
   guard reads the last issued document of the same kind and refuses an earlier
   date, with ARCO_ALLOW_BACKDATE as a loud, documented override.
   Verified: refuses a 01/07 invoice against FAC008 (23/07), accepts 23/08.

2. test/scripts/guard.ts — production opt-in. The sandbox-only guard had no way
   to express a deliberate production run, so any prod work meant bypassing it
   entirely (which is how guards die). Production now requires BOTH
   ARCO_ALLOW_PRODUCTION=<exact host> and
   ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD, and prints a banner. Nothing
   reaches prod by inheriting an ambient variable.
   Also fixes a misleading "(sandbox verified)" log that printed even on prod.

grantAgentRight.ts joins the repo (it was never committed) and gains --revoke,
so a temporarily elevated right can be handed back — used today to attach a
payment in production and revoked immediately after.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-26 01:26:12 +02:00
arcodangeandClaude Fable 5 fb13bdcc4f feat(write-skill): GED attach op — upload the source document onto its invoice (erp#43)
document-attach.sh uploads a source piece (the supplier's own PDF) onto an
invoice's GED via POST /documents/upload — idempotent by (object, filename,
sha256): before any POST the object's GED is listed and a same-named entry is
downloaded back and sha256-compared. Identical → deduped no-op; different
content → ABORT (refuse-never-repair, overwriteifexists always 0, never
Dolibarr's overwrite flag). Read-back after upload: re-list + download +
sha256-verify. Module-relative download paths are derived from the listing's
fullname (supplier invoices carry an id-derived get_exdir prefix like
9/2/FAF2026013/…, so reconstruction would be wrong).

Promote integration: new `attach` op in promote-plan/promote-apply (OP_SCRIPT),
object_id resolvable via @ref and #supplierinvoice lookups; a relative `file`
resolves against the manifest's directory (replay packs carry pdfs/ beside the
manifest, gitignored — README documents the books@ re-fetch message ids).
promote-plan prints each file's sha256 (or a loud MISSING) at review time.
CLI: `arcodange sandbox attach`.

Proof: offline case 12 in tests/run-tests.sh (upload body, dedupe, conflict
abort, field refusal, manifest-relative resolution via stubbed /documents);
live: manifest-C-ged-attach.json applied twice on the sandbox — run 1 four
created, run 2 four deduped, one GED file per FAF2026010-013, stored sha256s
equal to the re-fetched sources; tests/replay-idempotency.sh extended with an
attach op (4 created → 4 deduped, ged_files count unchanged) and a live
same-name/different-bytes abort verified.

Closes erp#43

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-19 00:11:59 +02:00
arcodangeandClaude Fable 5 3840e74dcd feat(write-skill): idempotency keys — manifest replay is a no-op (erp#44)
Learning #4 of the 2026-07-11 rehearsal: manifest B failed mid-run and could
not be re-applied — op 1 (the DARNIS invoice) had already run and a replay
would have duplicated it. Every write op now dedupes BEFORE any POST:

- thirdparty-create.sh: by exact name (promote '#thirdparty:name=' semantics);
  ambiguous (2+) aborts; an existing fiche missing the requested role aborts
  (refuse-never-repair). Emits {"id", "deduped"} instead of a bare id.
- invoice-create.sh: supplier kind by (socid, ref_supplier) — same key with a
  different total aborts as a conflict; customer kind (or supplier without
  ref_supplier) by (socid, date, total_ttc ±0.02, line fingerprint) with descs
  HTML-unescaped. Credit notes are never candidates. A deduped DRAFT with
  validate:true is validated on replay, so an interrupted run converges.
- payment-record.sh: by (invoice, amount, normalized transaction_id), composing
  with the erp#37 varchar(50) normalization on BOTH sides so historical
  long-form nums still match; same tx + different amount aborts; without a tx
  id there is no dedupe key (warned). Dedupe answers id:null (the payments list
  exposes no paiement rowid) + the existing bank line.
- All three refuse to POST blind when the dedupe lookup fails with anything but
  the documented empty-list 404 (the voir_tous trap would otherwise mint dupes).
- promote-apply.sh: marks each op created / deduped=true inline and totals them
  in the summary — an all-deduped second run is visible proof of a no-op.
- promote-plan.sh: advertises each op's dedupe key (and flags tx=MISSING as
  'a replay WILL double-pay').

Proof:
- tests/run-tests.sh: 5 new offline cases (11 total) — dedupe hits POST
  nothing, conflicts/ambiguity abort pre-POST, long-form history dedupes,
  draft convergence validates; stub extended to serve the new lookups with the
  live-observed empty behaviors ([] for invoices/payments, 404 for tiers).
- tests/replay-idempotency.sh (new, live): double-applies a self-contained
  manifest on the sandbox — run 1 '3 created' (rows 1/1/1), run 2 '3 deduped'
  with row counts unchanged and the stored num in erp#37 short form.
- The historic manifest-B now replays on the sandbox as 5/5 deduped, zero new
  rows — the exact replay Learning #4 declared impossible.

SKILL.md updated in the same change (per-op dedupe keys, replay-safety section,
gotchas); the 2026-07-11 runbook's Learning #4 carries a dated resolution
addendum.

Closes erp#44.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-18 23:46:05 +02:00
arcodangeandClaude Fable 5 35b227eb6d feat(write-skill): client-dossier ops — thirdparty update (allowlisted) + idempotent contacts
Part of erp#65 (phase 1). Ledger grammar "thirdparty complete" gets its
op: allowlisted non-ledger fields, per-field diff read-back. Contacts
are born idempotent (dedupe by email then name). Promote ops wired both
targets, offline stub tests, SKILL.md workflows, KM dossier manifest
(unsigned-contract truth fix + EIN-to-collect note).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 19:49:13 +02:00
arcodangeandClaude Fable 5 532143737b docs(replay-pack): prod replay executed 2026-07-15 — evidence in README
Fresh provenance 36/36 on re-fetched PDFs, prelude green, manifests A+B
applied (FAC004-007, FAF2026010-013, 6 payments), snapshots bracketed,
bank match since January: 0 UNKNOWN.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 19:15:33 +02:00
arcodangeandClaude Fable 5 ccc8b7912f 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]>
2026-07-12 14:05:01 +02:00
arcodange 0d66d6a6dc Merge pull request 'fix(txid): normalize bank tx ids to fit Dolibarr's num_payment varchar(50)' (#37) from arcodange/txid-varchar50-normalize into main
Reviewed-on: #37
2026-07-11 18:05:55 +02:00
arcodangeandClaude Fable 5 f9d83037b6 fix(txid): normalize bank tx ids to fit Dolibarr's num_payment varchar(50)
Qonto transaction ids run ~67 chars (<org>-<n>-<n>-transaction-<uuid>) but
Dolibarr stores num_payment in varchar(50) (llx_paiement.num_paiement,
llx_paiementfourn.num_paiement) — POSTing a payment with the raw id fails
HTTP 400 "value too long for type character varying(50)". Parade proven live
on the sandbox (2026-07-11): store the UUID suffix (globally unique, ~37
chars). Wise ids (short numerics) are unaffected.

Writer side — payment-record.sh strips everything through "transaction-"
before POST, announces the normalization on stderr, REFUSES (never truncates)
ids still >50 chars after normalization, and emits the normalized num in the
output JSON.

Reader side — bank-match.sh PASS 0 (exact tx-id, erp#28) now compares BOTH
sides in raw AND canonical short form: Qonto feed ids are carried long+short,
payment nums are normalized on compare — so nums stored short (the varchar(50)
form) and historical long-form nums both keep matching. Wise ids untouched.

Proven offline (no credentials, no network, no sandbox/prod writes):
- arcodange-bank-reco/tests/run-tests.sh — new bank-match --fixtures offline
  mode: long feed id ↔ short num, long ↔ long (back-compat), Wise numeric,
  each Δ+19d outside the ±7d window so only PASS 0 can pair them (exit 0,
  3×[tx-id]); plus the empty-num negative (exit 1, 0 matched).
- dolibarr-sandbox-write/tests/run-tests.sh — payment-record via a stubbed
  dol-write.sh (DOL_WRITE hook): long→short in POST body + output JSON, Wise
  untouched, >50-after-normalization refused BEFORE any POST, citing
  varchar(50).

Both SKILL.md document the canonical short form + the varchar(50) constraint.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 17:48:35 +02:00
arcodangeandClaude Fable 5 0240c519b1 fix(provision): grant user lire (251) to ai_agent_sandbox so the armed probe passes
The checkpoint status/relink-env armed probe calls GET /users/info, which
requires Dolibarr right 251 (user->user->lire). WRITE_IDS didn't include it,
so a freshly provisioned agent answered 403 on the probe — reported NOT armed
— while its key actually authenticates (GET /thirdparties -> 200). Right 251
was granted live in SQL on the sandbox (fk_user=4) today; this persists it in
WRITE_IDS so every future provision grants it.

Also teach both probes to tell the failure modes apart instead of one opaque
message: 401 = key rejected (stale/instance-encrypted -> re-provision),
403 = key OK but right 251 missing (-> grant it / re-provision), 200 = armed.
Docs updated accordingly (checkpoint SKILL.md probe outcomes, sandbox-write
SKILL.md gotcha, test/README.md rights table synced to WRITE_IDS incl. 262/111).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 17:38:30 +02:00
arcodangeandClaude Opus 4.7 b945c8de47 feat(payment): first-class transaction_id when recording a règlement
Make the originating bank transaction id a first-class input on payment-record.sh
so every règlement is tied to the real bank movement at write time.

- `transaction_id` is the canonical field (the Qonto/Wise feed tx id); `num` stays
  as a back-compat alias. It's stored on the payment's bank line (llx_bank.num_chq),
  the reconciliation key.
- Recording WITHOUT a transaction_id prints a stderr warning (still posts, but won't
  auto-reconcile) — nudges the agent to always carry it.
- Output normalises to {id, bank_transaction_id, transaction_id}.
- Promote: manifests' payment ops carry transaction_id; promote-plan shows it
  (tx=… or tx=MISSING).

Proven live: customer + supplier record with transaction_id; the `num` alias maps
to the same field; the no-tx warning fires; promote plan/apply carry it through.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 00:34:10 +02:00
arcodangeandClaude Opus 4.7 e748efd8f0 feat(payment): return the bank transaction id on règlements (reconciliation link)
A payment only returned its paiement id, which isn't what bank reconciliation
keys on. payment-record.sh now emits {id, bank_transaction_id, num}:

- bank_transaction_id = the Dolibarr bank line (llx_bank.fk_bank_line) the payment
  created, resolved via GET /{invoices|supplierinvoices}/{id}/payments (correlated
  by num, else the most recent line). Works for customer and supplier.
- num stores the originating bank tx id (Qonto/Wise) and lands on that bank line's
  num_chq — so arcodange-bank-reco can match a règlement to a statement line by id
  instead of fuzzy amount/date. Both ends captured at write time.

Proven live: customer {id:13,bank_transaction_id:35,num:QONTO-TX-1234},
supplier {id:16,bank_transaction_id:36,num:WISE-TX-5678}; llx_bank rows 35/36
carry the refs in num_chq. promote-apply still extracts .id unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 00:17:41 +02:00
arcodangeandClaude Opus 4.7 64d2cb4237 feat(skills,cli): supplier avoirs + banque lire (bank-account discovery)
The two V9 follow-ups, both proven live on the sandbox.

- creditnote-create.sh: `kind:"supplier"` makes an avoir fournisseur on
  /supplierinvoices (type=2 + fk_facture_source, carries ref_supplier); default
  customer path unchanged. Proven: customer AVC002 (-240) + supplier AVF2026001
  (-144, ref_supplier carried, linked to source, validated).
- bank-accounts.sh + `arcodange sandbox accounts`: list bank accounts (id/label/
  bank) so a payment can pick its account_id. Needs `banque lire` (rights 111),
  now added to the provisioner's WRITE_IDS so fresh runs include it; the existing
  ai_agent_sandbox user was granted it live. GET /bankaccounts now returns the 3
  accounts (QONTO, WISE EURO, Compte Courant Asso).
- SKILL.md: supplier-avoir example + accounts helper + updated banque-lire note.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 00:07:48 +02:00
arcodangeandClaude Opus 4.7 04985fe15c feat(promote): resolve pre-existing entities by business key (#entity:field=value)
Closes the last promote gap: a manifest can now reference records it does NOT
create. A value like "#thirdparty:name=KissMetrics" (or :code=CL0007) is looked
up on the TARGET at apply time and resolved to that target's id — so the same
manifest is portable (sandbox id on --target sandbox, prod id on --target prod).

promote-apply.sh: resolve() gains a "#" branch + a lookup() helper that queries
the target via the GET wrapper with sqlfilters. Supports thirdparty
(name/code/supplier_code) and invoice/supplierinvoice (ref/ref_supplier). A
lookup matching nothing OR more than one record ABORTS the run — it never
guesses, so it cannot write to the wrong entity.

Proven live: "#thirdparty:name=ACME Conseil" resolved to the existing client and
invoiced it; a not-found code and an ambiguous (2-match) name both aborted with
exit 1. Combined with @refs, arbitrary self-contained-or-referential change-sets
now replay cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 23:57:49 +02:00
arcodangeandClaude Opus 4.7 00d86b47a3 feat(skills,cli): promote-to-prod replay (ADR-0003 capstone) + supplier payment fix
The human-gated path that carries a reviewed sandbox change to prod.

- promote-plan.sh: render a manifest (JSON array of write ops with symbolic @refs
  instead of ids — portable sandbox->prod) as a human-readable change-set.
- promote-apply.sh <manifest> --target sandbox|prod: replay it, resolving each
  @ref to the id actually created during the run (dependent ops wire up). sandbox
  rehearses via dol-write.sh; prod via dol-prod-write.sh.
- dol-prod-write.sh: the ONLY prod-write path. Prod key read from the ENVIRONMENT
  only (DOLIBARR_PROD_WRITE_KEY, never a stored .env); every write refused unless
  ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD.
- create scripts take a DOL_WRITE override so promote-apply reuses them per target.
- bin/arcodange: `promote {plan|apply}` group + example manifest.
- payment-record.sh: fixed supplier payments (payment_mode_id + closepaidinvoices).

Proven live: plan renders; apply --target sandbox replays a 3-op chain with refs
resolved (@tp1->id, invoice socid=@tp1, payment invoice=@inv1); --target prod
without the confirm flag is REFUSED before sending. Supplier payment now works
end-to-end via the script.

Limitation (documented): manifests reference entities they create (@ref);
pre-existing prod entities need business-key resolution (follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 23:48:47 +02:00
arcodangeandClaude Opus 4.7 79286650d7 feat(skills,cli): sandbox avoir (credit note) + arcodange sandbox CLI group
- dolibarr-sandbox-write/scripts/creditnote-create.sh: create a customer avoir
  (credit note) — a customer invoice type=2 referencing source_invoice
  (fk_facture_source); amounts negative, validates to an AVC… ref. Proven live.
- bin/arcodange: new `sandbox` command group wiring the write scripts —
  `arcodange sandbox {thirdparty|invoice|payment|creditnote|write}` (JSON on
  stdin). Header + usage updated to note the CLI now does host-guarded sandbox
  writes (still read-only on prod).
- SKILL.md: avoir workflow + CLI notes.

Verified end-to-end through the CLI: thirdparty -> invoice (FAC…) -> avoir
(AVC…, total_ttc -240, fk_facture_source set); host-guard intact via the CLI.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 21:04:49 +02:00
arcodangeandClaude Opus 4.7 d2e8b3a3a4 feat(skills): dolibarr-sandbox-write — host-guarded write skill (V9)
The write-capable companion to the read-only dolibarr* skills, scoped to the
erp-sandbox. Lets an AI agent rehearse bookkeeping writes against a copy of prod
(ADR-0003) before a human promotes the reviewed change to prod.

- scripts/dol-write.sh: write wrapper that REFUSES any host that is not
  erp-sandbox.arcodange.lab (the structural prod-safety guarantee) using the
  ai_agent_sandbox key from a gitignored .env.
- scripts/thirdparty-create.sh: create client/supplier fiches; codes auto-assign
  via the elephant mask (code="-1").
- scripts/invoice-create.sh: customer (/invoices) or supplier (/supplierinvoices)
  invoices with product/service lines + ref_supplier, optional validate.
- scripts/payment-record.sh: record a règlement (VIR/CB/CHQ/LIQ); customer pays
  full + marks paid, supplier needs an amount.
- SKILL.md (safety model + workflows + the human-gated promote flow), .env.example,
  example input.

Proven end-to-end live against the sandbox: client -> invoice (service+product
lines, HT 1100 / TTC 1320) -> validate -> payment (paid); supplier -> supplier
invoice (ref_supplier carried) -> validate. Host guard verified to refuse a prod
URL before sending.

Avoirs (credit notes) and bin/arcodange CLI wiring are planned follow-ups.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 20:49:31 +02:00