#!/usr/bin/env bash # Update an EXISTING thirdparty's DOSSIER fields in the SANDBOX — allowlisted. # # The ledger grammar's "thirdparty complete": a fiche is completed with identity / # address / typology / notes / national ids — never with ledger-side state. This # script therefore REFUSES any field outside the allowlist below (code_client, # client/fournisseur roles, remises, payment conditions, bank details… are # creation- or ledger-side concerns, not dossier updates). Refusal happens BEFORE # any request is sent, exits non-zero, and names the offending key(s). # # Usage: # thirdparty-update.sh # thirdparty-update.sh - # fields JSON on stdin # thirdparty-update.sh # promote form: {"socid":N,"fields":{…}} on stdin # # Read-back contract (anti-silent-partial-apply): # GET before → PUT → GET after. stdout is a JSON diff of the REQUESTED fields # that actually changed: {"id":N,"changed":{field:{"before":…,"after":…}}}. # A requested field whose read-back differs from the requested value makes the # script exit non-zero — a write that "didn't take" is an error, never a silent # partial apply. Re-running the same update yields "changed": {} (idempotent). # # All requests go through dol-write.sh (or $DOL_WRITE), which refuses any host # that is not the sandbox. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" W="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}" SOCID_ARG="${1:-}" SRC="${2:-}" if [[ -n "${SOCID_ARG}" ]]; then if [[ -z "${SRC}" || "${SRC}" == "-" ]]; then INPUT="$(cat)" elif [[ -f "${SRC}" ]]; then INPUT="$(cat "${SRC}")" else INPUT="${SRC}"; fi # inline JSON else INPUT="$(cat)" # promote form: {"socid":N,"fields":{…}} fi TMPD="$(mktemp -d -t tpupd.XXXXXX)"; trap 'rm -rf "${TMPD}"' EXIT # --- 1. Validate against the dossier allowlist (before ANY request) ------------ cat > "${TMPD}/validate.py" <<'PY' import json, sys # The dossier allowlist — identity / address / typology / notes / national ids. # Everything else is refused: ledger-side or creation-side fields have their own # ops, and silently forwarding them would let a typo mutate the ledger. ALLOWED = ["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", "idprof2", "idprof3", "idprof4", "idprof5", "idprof6", "tva_intra"] socid_arg = sys.argv[1] try: d = json.loads(sys.stdin.read() or "null") except Exception as e: sys.exit("thirdparty-update.sh: input is not valid JSON: %s" % e) if socid_arg: socid, fields = socid_arg, d else: if not isinstance(d, dict) or "socid" not in d or "fields" not in d: sys.exit("thirdparty-update.sh: stdin form must be {\"socid\":N,\"fields\":{...}}") socid, fields = d["socid"], d["fields"] socid = str(socid) if not socid.isdigit(): sys.exit("thirdparty-update.sh: socid must be numeric, got %r" % socid) if not isinstance(fields, dict) or not fields: sys.exit("thirdparty-update.sh: no fields to update (fields must be a non-empty JSON object)") bad = sorted(k for k in fields if k not in ALLOWED) if bad: sys.exit("thirdparty-update.sh: REFUSED — field(s) outside the dossier allowlist: %s\n" " allowed: %s" % (", ".join(bad), ", ".join(ALLOWED))) print(socid) print(json.dumps(fields, ensure_ascii=False)) PY MAPPED="$(printf '%s' "${INPUT}" | python3 "${TMPD}/validate.py" "${SOCID_ARG}")" SOCID="$(sed -n 1p <<<"${MAPPED}")" BODY="$(sed -n 2p <<<"${MAPPED}")" printf '%s' "${BODY}" > "${TMPD}/requested.json" # --- 2. GET before → PUT → GET after ------------------------------------------- "${W}" GET "/thirdparties/${SOCID}" > "${TMPD}/before.json" "${W}" PUT "/thirdparties/${SOCID}" "${BODY}" > /dev/null "${W}" GET "/thirdparties/${SOCID}" > "${TMPD}/after.json" # --- 3. Per-field diff of the requested fields; fail if a field didn't take ---- cat > "${TMPD}/diff.py" <<'PY' import json, sys before = json.load(open(sys.argv[1])) after = json.load(open(sys.argv[2])) req = json.load(open(sys.argv[3])) socid = int(sys.argv[4]) def s(v): # Dolibarr returns most scalars as strings; None ≡ "" return "" if v is None else str(v) changed, failed = {}, [] for k, want in req.items(): b, a = before.get(k), after.get(k) if s(a) != s(want): failed.append((k, want, a)) if s(b) != s(a): changed[k] = {"before": b, "after": a} print(json.dumps({"id": socid, "changed": changed}, ensure_ascii=False)) if failed: for k, want, got in failed: sys.stderr.write("thirdparty-update.sh: field %r did NOT take — requested %r, read back %r\n" % (k, want, got)) sys.exit(1) PY python3 "${TMPD}/diff.py" "${TMPD}/before.json" "${TMPD}/after.json" "${TMPD}/requested.json" "${SOCID}"