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
197 lines
8.3 KiB
Bash
Executable File
197 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Offline stand-in for dol-write.sh, used ONLY by tests/run-tests.sh (injected via
|
|
# the DOL_WRITE env hook). Never talks to any host — sandbox and prod are both out
|
|
# of reach by construction. Dispatch by endpoint:
|
|
#
|
|
# GET …/payments → serves $STUB_STATE/payments.json when present; else
|
|
# (post-POST correlate) a list built from post_body.json;
|
|
# else [] — the live sandbox answers [] (200) when empty
|
|
# GET /thirdparties?… → name-lookup: serves thirdparties.json, else the
|
|
# Dolibarr empty behavior (HTTP 404 + non-zero)
|
|
# GET /thirdparties/<id> → serves $STUB_STATE/thirdparty.json (or a canned
|
|
# "before" fiche on first read)
|
|
# PUT /thirdparties/<id> → records put_body.json/put_endpoint, merges the body
|
|
# into thirdparty.json (so the read-after sees it)
|
|
# POST /thirdparties → records tp_post_body.json, echoes 90
|
|
# GET /supplierinvoices?… → dedupe listing: serves supplierinvoices.json else []
|
|
# GET /invoices?… → dedupe listing: serves invoices.json else []
|
|
# GET /supplierinvoices/<id> · /invoices/<id>
|
|
# → serves invoice_detail_<id>.json, else HTTP 404
|
|
# GET /contacts… → serves $STUB_STATE/contacts.json; mimics Dolibarr's
|
|
# empty-list behavior (HTTP 404 + non-zero) when absent
|
|
# POST /contacts… → records contact_post_body.json, appends the contact
|
|
# (id 88) to contacts.json, echoes 88
|
|
# POST …/validate → records validated_endpoint (proof validation ran)
|
|
# POST <anything else> → payment/invoice behavior: records post_body.json/
|
|
# post_endpoint, echoes 77 (unchanged from erp#37)
|
|
# GET <anything else> → payments list served back from post_body.json
|
|
set -euo pipefail
|
|
STATE="${STUB_STATE:?stub-dol-write.sh: STUB_STATE not set}"
|
|
METHOD="$1"; ENDPOINT="$2"; BODY="${3:-}"
|
|
# dol-write.sh accepts @file bodies (document-attach.sh uses one for the base64
|
|
# payload) — dereference it here so the recorded body is the JSON, not "@/path".
|
|
[[ "${BODY}" == @* ]] && BODY="$(cat "${BODY:1}")"
|
|
case "${METHOD} ${ENDPOINT}" in
|
|
GET\ *"/payments"*)
|
|
if [[ -f "${STATE}/payments.json" ]]; then
|
|
cat "${STATE}/payments.json"
|
|
elif [[ -f "${STATE}/post_body.json" ]]; then
|
|
python3 - "${STATE}/post_body.json" <<'PY'
|
|
import json, sys
|
|
body = json.load(open(sys.argv[1]))
|
|
print(json.dumps([{"num": body.get("num_payment", ""),
|
|
"amount": body.get("amount", ""),
|
|
"date": "2026-06-20 12:00:00",
|
|
"fk_bank_line": "556"}]))
|
|
PY
|
|
else
|
|
printf '[]' # live sandbox: empty payment list is [] with HTTP 200
|
|
fi
|
|
;;
|
|
"GET /thirdparties?"*)
|
|
if [[ -f "${STATE}/thirdparties.json" ]]; then
|
|
cat "${STATE}/thirdparties.json"
|
|
else
|
|
# Dolibarr answers 404 (not []) when a thirdparty lookup matches nothing.
|
|
printf '%s' '{"error":{"code":404,"message":"Not Found: No third parties found"}}'
|
|
echo "stub-dol-write.sh: HTTP 404 on GET ${ENDPOINT}" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
"GET /thirdparties/"*)
|
|
if [[ -f "${STATE}/thirdparty.json" ]]; then
|
|
cat "${STATE}/thirdparty.json"
|
|
else
|
|
printf '%s' '{"id":"1","name":"KissMetrics","name_alias":null,"email":"","note_public":"OLD NOTE (pre-dossier)","zip":null,"town":null}'
|
|
fi
|
|
;;
|
|
"PUT /thirdparties/"*)
|
|
printf '%s' "${BODY}" > "${STATE}/put_body.json"
|
|
printf '%s\n' "${ENDPOINT}" > "${STATE}/put_endpoint"
|
|
python3 - "${STATE}" "${BODY}" <<'PY'
|
|
import json, os, sys
|
|
state, body = sys.argv[1], json.loads(sys.argv[2])
|
|
p = os.path.join(state, "thirdparty.json")
|
|
cur = (json.load(open(p)) if os.path.exists(p)
|
|
else {"id": "1", "name": "KissMetrics", "name_alias": None, "email": "",
|
|
"note_public": "OLD NOTE (pre-dossier)", "zip": None, "town": None})
|
|
cur.update(body)
|
|
json.dump(cur, open(p, "w"), ensure_ascii=False)
|
|
print(json.dumps(cur, ensure_ascii=False))
|
|
PY
|
|
;;
|
|
"POST /thirdparties")
|
|
printf '%s' "${BODY}" > "${STATE}/tp_post_body.json"
|
|
echo "90"
|
|
;;
|
|
"GET /supplierinvoices?"*)
|
|
if [[ -f "${STATE}/supplierinvoices.json" ]]; then cat "${STATE}/supplierinvoices.json"; else printf '[]'; fi
|
|
;;
|
|
"GET /invoices?"*)
|
|
if [[ -f "${STATE}/invoices.json" ]]; then cat "${STATE}/invoices.json"; else printf '[]'; fi
|
|
;;
|
|
"GET /supplierinvoices/"*|"GET /invoices/"*)
|
|
ID="${ENDPOINT##*/}"
|
|
if [[ -f "${STATE}/invoice_detail_${ID}.json" ]]; then
|
|
cat "${STATE}/invoice_detail_${ID}.json"
|
|
else
|
|
printf '%s' '{"error":{"code":404,"message":"Not Found"}}'
|
|
echo "stub-dol-write.sh: HTTP 404 on GET ${ENDPOINT}" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
"GET /contacts"*)
|
|
if [[ -f "${STATE}/contacts.json" ]]; then
|
|
cat "${STATE}/contacts.json"
|
|
else
|
|
# Dolibarr's list endpoints answer 404 (not []) when nothing matches.
|
|
printf '%s' '{"error":{"code":404,"message":"No contact found"}}'
|
|
echo "stub-dol-write.sh: HTTP 404 on GET ${ENDPOINT}" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
"POST /contacts"*)
|
|
printf '%s' "${BODY}" > "${STATE}/contact_post_body.json"
|
|
python3 - "${STATE}" "${BODY}" <<'PY'
|
|
import json, os, sys
|
|
state, body = sys.argv[1], json.loads(sys.argv[2])
|
|
p = os.path.join(state, "contacts.json")
|
|
rows = json.load(open(p)) if os.path.exists(p) else []
|
|
body = dict(body); body["id"] = "88"
|
|
rows.append(body)
|
|
json.dump(rows, open(p, "w"), ensure_ascii=False)
|
|
PY
|
|
echo "88"
|
|
;;
|
|
"GET /documents/download?"*)
|
|
# Serve document_download.json when staged; else (post-upload read-back)
|
|
# synthesize from upload_body.json; else the live 404.
|
|
if [[ -f "${STATE}/document_download.json" ]]; then
|
|
cat "${STATE}/document_download.json"
|
|
elif [[ -f "${STATE}/upload_body.json" ]]; then
|
|
python3 - "${STATE}/upload_body.json" <<'PY'
|
|
import base64, json, sys
|
|
b = json.load(open(sys.argv[1]))
|
|
print(json.dumps({"filename": b.get("filename"), "content-type": "application/pdf",
|
|
"filesize": len(base64.b64decode(b.get("filecontent", ""))),
|
|
"content": b.get("filecontent", "")}))
|
|
PY
|
|
else
|
|
printf '%s' '{"error":{"code":404,"message":"Not Found"}}'
|
|
echo "stub-dol-write.sh: HTTP 404 on GET ${ENDPOINT}" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
"GET /documents?"*)
|
|
# Serve documents.json when staged; else (post-upload read-back) a listing
|
|
# built from upload_body.json — with the supplier-invoice get_exdir prefix
|
|
# (0/3/REF/…) so the relative-path derivation is exercised; else the live
|
|
# Dolibarr behavior: an object with no documents answers HTTP 404, not [].
|
|
if [[ -f "${STATE}/documents.json" ]]; then
|
|
cat "${STATE}/documents.json"
|
|
elif [[ -f "${STATE}/upload_body.json" ]]; then
|
|
python3 - "${STATE}/upload_body.json" <<'PY'
|
|
import base64, json, sys
|
|
b = json.load(open(sys.argv[1]))
|
|
ref, fn = b.get("ref", "REF"), b.get("filename", "file.pdf")
|
|
print(json.dumps([{"name": None, "relativename": fn, "type": "file",
|
|
"level1name": ref,
|
|
"fullname": "/var/www/documents/fournisseur/facture/0/3/%s/%s" % (ref, fn),
|
|
"size": len(base64.b64decode(b.get("filecontent", "")))}]))
|
|
PY
|
|
else
|
|
printf '%s' '{"error":{"code":404,"message":"Not Found: no document"}}'
|
|
echo "stub-dol-write.sh: HTTP 404 on GET ${ENDPOINT}" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
"POST /documents/upload")
|
|
printf '%s' "${BODY}" > "${STATE}/upload_body.json"
|
|
python3 - "${STATE}/upload_body.json" <<'PY'
|
|
import json, sys
|
|
print(json.dumps(json.load(open(sys.argv[1])).get("filename")))
|
|
PY
|
|
;;
|
|
POST\ *"/validate")
|
|
printf '%s\n' "${ENDPOINT}" > "${STATE}/validated_endpoint"
|
|
echo '{"success":1}'
|
|
;;
|
|
POST\ *)
|
|
printf '%s' "${BODY}" > "${STATE}/post_body.json"
|
|
printf '%s\n' "${ENDPOINT}" > "${STATE}/post_endpoint"
|
|
echo "77"
|
|
;;
|
|
GET\ *)
|
|
python3 - "${STATE}/post_body.json" <<'PY'
|
|
import json, sys
|
|
body = json.load(open(sys.argv[1]))
|
|
print(json.dumps([{"num": body.get("num_payment", ""),
|
|
"date": "2026-06-20 12:00:00",
|
|
"fk_bank_line": "556"}]))
|
|
PY
|
|
;;
|
|
*)
|
|
echo "stub-dol-write.sh: unexpected method ${METHOD}" >&2; exit 2
|
|
;;
|
|
esac
|