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]>
This commit is contained in:
@@ -4,16 +4,19 @@ description: >-
|
||||
WRITE operations against the Arcodange Dolibarr SANDBOX (erp-sandbox.arcodange.lab)
|
||||
— the rehearsal environment where an AI agent records thirdparties, invoices and
|
||||
payments before any change is promoted to prod. Create client/supplier fiches
|
||||
(auto-coded), customer + supplier invoices with product/service lines and the
|
||||
(auto-coded), update an existing fiche's dossier (allowlisted non-ledger fields
|
||||
with a before/after read-back diff), add contacts idempotently (dedupe by email
|
||||
then name), customer + supplier invoices with product/service lines and the
|
||||
supplier's own reference, validate them, and record règlements (payments). Every
|
||||
write goes through dol-write.sh, which REFUSES any host that is not the sandbox —
|
||||
the structural guarantee (ADR-0003) that this skill can never mutate production.
|
||||
Use when the user asks to "create a thirdparty / supplier / client fiche", "saisir
|
||||
une facture", "record an invoice with lines", "enregistrer un règlement / paiement",
|
||||
or to rehearse a write before promoting it to prod. SKIP for production writes
|
||||
Use when the user asks to "create a thirdparty / supplier / client fiche",
|
||||
"compléter / mettre à jour la fiche client", "add a contact to a thirdparty",
|
||||
"saisir une facture", "record an invoice with lines", "enregistrer un règlement /
|
||||
paiement", or to rehearse a write before promoting it to prod. SKIP for production writes
|
||||
(prod stays read-only via the `dolibarr` skill's `ai_agent` key; promotion is a
|
||||
separate, human-gated replay), and for credit notes/avoirs (a planned follow-up).
|
||||
Depends on the write-scoped `ai_agent_sandbox` Dolibarr user + its API key.
|
||||
separate, human-gated replay). Depends on the write-scoped `ai_agent_sandbox`
|
||||
Dolibarr user + its API key.
|
||||
requires:
|
||||
bins: [bash, curl, python3]
|
||||
auth: ".env with DOLIBARR_SANDBOX_URL + DOLIBARR_SANDBOX_API_KEY (mode 600, gitignored)"
|
||||
@@ -62,7 +65,8 @@ GET /status` should return HTTP 200 with `"environment":"non-production"`.
|
||||
|
||||
## Workflows
|
||||
|
||||
All three read a JSON object on **stdin** (or a file path as `$1`) and emit ids.
|
||||
Each script reads a JSON object on **stdin** (or a file path — or inline JSON —
|
||||
as `$1`; `thirdparty-update.sh` also takes `<socid>` first) and emits ids as JSON.
|
||||
|
||||
### 1 · Thirdparty (fiche client/fournisseur) — `scripts/thirdparty-create.sh`
|
||||
|
||||
@@ -148,13 +152,49 @@ come out negative. `kind:"supplier"` targets `/supplierinvoices` (carry
|
||||
(`AVC…` for customer, `AVF…` for supplier). Emits `{id, ref, ref_supplier, total_ht,
|
||||
total_ttc, fk_facture_source, statut}`.
|
||||
|
||||
### 5 · Thirdparty dossier update — `scripts/thirdparty-update.sh`
|
||||
|
||||
```sh
|
||||
scripts/thirdparty-update.sh 1 '{"email":"[email protected]","note_public":"…"}'
|
||||
scripts/thirdparty-update.sh 1 fiche.json # or a file; or "-" for stdin
|
||||
echo '{"socid":1,"fields":{"zip":"33713"}}' | scripts/thirdparty-update.sh # promote form
|
||||
```
|
||||
Updates an **existing** fiche's dossier — **allowlisted fields only**: `name`,
|
||||
`name_alias`, `address`, `zip`, `town`, `state_id`, `region_id`, `country_id`,
|
||||
`country_code`, `email`, `url`, `phone`, `typent_id`, `effectif_id`, `note_public`,
|
||||
`note_private`, `idprof1`…`idprof6`, `tva_intra`. That is the ledger grammar's
|
||||
"thirdparty complete" — identity / address / typology / notes / national ids.
|
||||
Anything else (`code_client`, `client`/`fournisseur` roles, remises, payment
|
||||
conditions…) is **refused before any request**, exit non-zero, offender named.
|
||||
Read-back contract: GET before → PUT → GET after; emits
|
||||
`{"id":N,"changed":{field:{"before":…,"after":…}}}` restricted to the requested
|
||||
fields that actually changed, and exits non-zero if a requested field did not
|
||||
take. Re-applying the same update yields `"changed": {}` — idempotency you can see.
|
||||
|
||||
### 6 · Contact — `scripts/contact-create.sh`
|
||||
|
||||
```sh
|
||||
echo '{"socid":1,"lastname":"Rootering","firstname":"Hendrik","poste":"COO",
|
||||
"email":"[email protected]"}' | scripts/contact-create.sh
|
||||
```
|
||||
**Idempotent from day one** (the erp#44 pattern): before any POST it lists the
|
||||
thirdparty's contacts and dedupes **by case-insensitive email, then by
|
||||
(lastname, firstname)**; a match emits `{"id": <existing>, "deduped": true}` and
|
||||
exits 0 without writing. Otherwise POST `/contacts` → `{"id": <new>,
|
||||
"deduped": false}`. Required: `socid`, `lastname`. Optional: `firstname`, `poste`
|
||||
(job title), `email`, `phone` (stored as `phone_pro`), `phone_mobile`,
|
||||
`phone_perso`, `address`, `zip`, `town`, `country_id`, `note_public`,
|
||||
`note_private`. Unknown fields are refused, never dropped.
|
||||
|
||||
## Promote to prod (rehearse → review → replay)
|
||||
|
||||
The ADR-0003 capstone: take a change rehearsed in the sandbox and apply the **same
|
||||
operations** to prod, with a human in the loop. The unit is a **manifest** — a JSON
|
||||
array of write ops using **symbolic refs** (`@name`) instead of ids, so it is
|
||||
portable from sandbox to prod (an invoice references `@tp1`, the thirdparty created
|
||||
earlier in the run). See `examples/promote-manifest.json`.
|
||||
earlier in the run). See `examples/promote-manifest.json`. Op kinds: `thirdparty`,
|
||||
`thirdparty_update` (input: `socid` + `fields`), `contact`, `invoice`, `creditnote`,
|
||||
`payment` — each mapping to its workflow script above.
|
||||
|
||||
```sh
|
||||
scripts/promote-plan.sh change.json # 1. human-readable review
|
||||
@@ -207,5 +247,18 @@ sandbox KissMetrics on `--target sandbox` and the prod one on `--target prod`.
|
||||
- **Avoirs (credit notes)** → `creditnote-create.sh` (customer invoice `type=2`
|
||||
referencing `source_invoice`; amounts negative, ref `AVC…`). Supplier avoirs
|
||||
are a follow-up.
|
||||
- **Dossier updates are allowlisted by design.** The ledger grammar's "thirdparty
|
||||
complete" completes a fiche with identity/address/typology/notes/idprof1-6/
|
||||
tva_intra — never with ledger-side state. `thirdparty-update.sh` refuses
|
||||
anything else (`code_client`, roles, payment conditions…) *before* any request:
|
||||
a typo'd field must fail loudly, not silently mutate the ledger. Offline proof
|
||||
in `tests/run-tests.sh`.
|
||||
- **Contacts dedupe by (socid, email) then (socid, lastname+firstname)**, both
|
||||
case-insensitive — `contact-create.sh` answers `{"id": …, "deduped": true}`
|
||||
instead of minting a duplicate, so replaying a manifest is always safe.
|
||||
- **`poste`, not `soc2`.** The job-title field on a Dolibarr contact is `poste`;
|
||||
`soc2` (seen in WIP operator payloads) is not a Dolibarr field and the API
|
||||
would drop it silently — `contact-create.sh` refuses it with a pointer to
|
||||
`poste`.
|
||||
- **CLI:** all of these are also `arcodange sandbox {thirdparty|invoice|payment|creditnote|write}`
|
||||
(JSON on stdin) — `arcodange sandbox help` for the list.
|
||||
|
||||
Reference in New Issue
Block a user