Files
erp/.claude/skills/arcodange-bank-reco/scripts/bank-curl.sh
Gabriel Radureau bd90266372 add arcodange-bank-reco — Qonto + Wise reconciliation against Dolibarr
V6 — the first cross-system skill (under arcodange-* not dolibarr-*).
Closes the loop between what Dolibarr says (ERP-internal) and what the
bank actually saw.

What ships:
- arcodange-bank-reco/scripts/bank-curl.sh        unified read-only wrapper for Qonto + Wise
- arcodange-bank-reco/scripts/bank-probe.sh       auth + discovery (org slug, profile id, balances)
- arcodange-bank-reco/scripts/qonto-transactions  Qonto txn lister with pagination + filters
- arcodange-bank-reco/scripts/wise-transactions   Wise activity lister with --enrich for wire refs
- arcodange-bank-reco/scripts/bank-match.sh       3-bucket reconciliation (matched/bank-only/dol-only)
                                                  with internal Wise↔Qonto consolidation detection
- arcodange-bank-reco/scripts/bank-balance.sh     live balances + Dolibarr cumulative-by-fk_account

The headline bank-curl.sh is SCA-aware (Wise RSA dance) even though we
don't end up using it: the EU statement endpoint is region-blocked
("Funding transfers and retrieving balance statements via API are not
supported except for accounts based in the US, Canada, Australia, New
Zealand, Singapore, and Malaysia" per Wise docs). The wrapper supports
SCA so when/if Wise opens it, we're ready.

The pivot that unblocked Wise incoming: /v1/profiles/{pid}/activities
(documented at https://docs.wise.com/api-reference/activity/activitylist.md)
returns ALL movements in a unified HTML-tagged feed, no SCA required.
Parsing strips the HTML and recovers structured amount/sign/currency.

CLI integration:
- bin/arcodange bank {probe,qonto-transactions,wise-transactions,match,balance,curl}
- dolibarr/SKILL.md catalogue + Pointers updated
- dolibarr/README.md env schema extended with QONTO_*, WISE_*

Live baseline findings to raise with the cohort review (captured in
examples/bank-match-2026-01-to-05.txt):
- Wise 2026-05-29 +2147 EUR Kissmetrics NOT YET in Dolibarr
- Qonto bank-only: MISTRAL.AI 172.68, CLAUDE.AI 180, URSSAF 493, FOUREZ +1000
- 6 movements matched cleanly across Jan-May 2026
- Wise→Qonto 5000 EUR consolidation on 2026-03-13 auto-detected as internal
- Live balance: Qonto 4191.54 + Wise 5308.25 = 9499.79 EUR

V7 candidates noted in SKILL.md out-of-scope: reference-based matching
via the Wise --enrich wire refs (FOR INVOICE FAC***), multi-row Dolibarr
sub-payment aggregation, smarter avoir cycle handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 13:57:21 +02:00

108 lines
3.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Read-only curl wrapper for the two banks Arcodange uses: Qonto + Wise.
#
# Usage:
# bank-curl.sh qonto <path> # e.g. bank-curl.sh qonto /v2/organization
# bank-curl.sh wise <path> # e.g. bank-curl.sh wise /v2/profiles
# bank-curl.sh -i <bank> <path> # include curl's -i (response headers)
#
# Reads credentials from ../../dolibarr/.env (the shared canonical file
# used by every dolibarr-* and arcodange-* skill). Required vars:
# QONTO_LOGIN, QONTO_SECRET_KEY (for qonto)
# WISE_API_TOKEN (for wise)
#
# Exits non-zero on HTTP >=400 and writes the body to stdout + a short
# "bank-curl.sh: HTTP <code>" message to stderr — same shape as dol-curl.sh.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SCRIPT_DIR}/../../dolibarr/.env"
if [[ ! -f "${ENV_FILE}" ]]; then
echo "bank-curl.sh: missing ${ENV_FILE}" >&2
echo " See dolibarr/README.md for the .env schema; arcodange-bank-reco extends it" >&2
echo " with QONTO_LOGIN, QONTO_SECRET_KEY, WISE_API_TOKEN, WISE_PROFILE_ID." >&2
exit 2
fi
set -a; source "${ENV_FILE}"; set +a
PASSTHRU=()
while [[ $# -gt 2 ]]; do
PASSTHRU+=("$1"); shift
done
if [[ $# -lt 2 ]]; then
echo "bank-curl.sh: usage: bank-curl.sh [curl-opts] <qonto|wise> <api-path>" >&2
exit 2
fi
BANK="$1"; API_PATH="$2"
case "${BANK}" in
qonto)
: "${QONTO_LOGIN:?bank-curl.sh: QONTO_LOGIN not set in .env}"
: "${QONTO_SECRET_KEY:?bank-curl.sh: QONTO_SECRET_KEY not set in .env}"
BASE="https://thirdparty.qonto.com"
AUTH_HEADER="Authorization: ${QONTO_LOGIN}:${QONTO_SECRET_KEY}"
;;
wise)
: "${WISE_API_TOKEN:?bank-curl.sh: WISE_API_TOKEN not set in .env}"
BASE="https://api.wise.com"
AUTH_HEADER="Authorization: Bearer ${WISE_API_TOKEN}"
;;
*)
echo "bank-curl.sh: unknown bank '${BANK}' (use qonto or wise)" >&2
exit 2
;;
esac
BODY_FILE="$(mktemp -t bankcurl.XXXXXX)"
HEADERS_FILE="$(mktemp -t bankcurlhdr.XXXXXX)"
trap 'rm -f "${BODY_FILE}" "${HEADERS_FILE}"' EXIT
do_call() {
local extra_header_1="${1:-}" extra_header_2="${2:-}"
local extra_args=()
[[ -n "${extra_header_1}" ]] && extra_args+=("-H" "${extra_header_1}")
[[ -n "${extra_header_2}" ]] && extra_args+=("-H" "${extra_header_2}")
curl -sS \
-H "${AUTH_HEADER}" \
-H "Accept: application/json" \
--max-time 30 \
-o "${BODY_FILE}" \
-D "${HEADERS_FILE}" \
-w "%{http_code}" \
${PASSTHRU[@]+"${PASSTHRU[@]}"} \
${extra_args[@]+"${extra_args[@]}"} \
"${BASE}${API_PATH}"
}
HTTP_CODE=$(do_call)
# Wise SCA flow: a 403 with x-2fa-approval-result: REJECTED + x-2fa-approval: <token>
# header means the endpoint is sensitive and the call must be signed with our
# registered RSA private key. We sign the one-time token and retry.
if [[ "${BANK}" == "wise" && "${HTTP_CODE}" == "403" ]]; then
ONE_TIME=$(awk 'tolower($1) == "x-2fa-approval:" { gsub(/[\r\n]/, "", $2); print $2; exit }' "${HEADERS_FILE}")
if [[ -n "${ONE_TIME}" ]]; then
KEY_PATH="${WISE_SCA_KEY_PATH:-}"
# Expand ~ if used
KEY_PATH="${KEY_PATH/#\~/$HOME}"
if [[ -z "${KEY_PATH}" || ! -f "${KEY_PATH}" ]]; then
echo "bank-curl.sh: Wise endpoint requires SCA but WISE_SCA_KEY_PATH is missing." >&2
echo " Generate a keypair, upload the public key to Wise, set WISE_SCA_KEY_PATH in .env." >&2
echo " See arcodange-bank-reco/SKILL.md for the setup steps." >&2
cat "${BODY_FILE}"
exit 1
fi
SIGNATURE=$(printf '%s' "${ONE_TIME}" | openssl dgst -sha256 -sign "${KEY_PATH}" | base64 | tr -d '\n')
HTTP_CODE=$(do_call "x-2fa-approval: ${ONE_TIME}" "X-Signature: ${SIGNATURE}")
fi
fi
cat "${BODY_FILE}"
if [[ "${HTTP_CODE}" -ge 400 ]]; then
echo "bank-curl.sh: HTTP ${HTTP_CODE} on ${BANK}${API_PATH}" >&2
exit 1
fi