#!/usr/bin/env bash # List Qonto transactions for a period, on one bank account. # # Usage: # qonto-transactions.sh [--since YYYY-MM-DD] [--until YYYY-MM-DD] # [--account ] # default: first active account # [--month YYYY-MM] # [--side credit|debit] # filter # [--json] # raw JSON, no table # # Output: a compact table — date | side | amount | currency | op type | label. # Pagination is followed automatically. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BANK_CURL="${SCRIPT_DIR}/bank-curl.sh" SINCE=""; UNTIL=""; MONTH=""; ACCOUNT=""; SIDE=""; FMT="table" while [[ $# -gt 0 ]]; do case "$1" in --since) SINCE="$2"; shift 2 ;; --until) UNTIL="$2"; shift 2 ;; --month) MONTH="$2"; shift 2 ;; --account) ACCOUNT="$2"; shift 2 ;; --side) SIDE="$2"; shift 2 ;; --json) FMT="json"; shift ;; -h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "qonto-transactions.sh: unknown arg: $1" >&2; exit 2 ;; esac done # Default window: --month overrides --since/--until; otherwise last 90 days. if [[ -n "${MONTH}" ]]; then SINCE="${MONTH}-01" # Last day of month — works for any month using python UNTIL="$(python3 -c "import datetime,calendar; y,m=map(int,'${MONTH}'.split('-')); print(f'{y:04d}-{m:02d}-{calendar.monthrange(y,m)[1]:02d}')")" fi [[ -z "${SINCE}" ]] && SINCE="$(python3 -c "import datetime; print((datetime.date.today()-datetime.timedelta(days=90)).strftime('%Y-%m-%d'))")" [[ -z "${UNTIL}" ]] && UNTIL="$(python3 -c "import datetime; print(datetime.date.today().strftime('%Y-%m-%d'))")" # Discover default account if not specified if [[ -z "${ACCOUNT}" ]]; then TMP_ORG=$(mktemp -t qontoorg.XXXXXX.json) trap 'rm -f "${TMP_ORG}"' EXIT "${BANK_CURL}" qonto /v2/organization > "${TMP_ORG}" ACCOUNT=$(python3 -c " import json, sys d = json.load(open(sys.argv[1])) accs = (d.get('organization') or {}).get('bank_accounts') or [] active = [a for a in accs if a.get('status') == 'active'] if not active: sys.exit('no active account') print(active[0]['id']) " "${TMP_ORG}") fi # Paginate TMP_ALL=$(mktemp -t qontoall.XXXXXX.json) trap 'rm -f "${TMP_ORG:-/dev/null}" "${TMP_ALL}"' EXIT echo '[]' > "${TMP_ALL}" page=1 while :; do TMP_PAGE=$(mktemp -t qontop.XXXXXX.json) Q="bank_account_id=${ACCOUNT}&settled_at_from=${SINCE}T00:00:00Z&settled_at_to=${UNTIL}T23:59:59Z&per_page=100¤t_page=${page}" "${BANK_CURL}" qonto "/v2/transactions?${Q}" > "${TMP_PAGE}" # Merge transactions into the all-array python3 - "${TMP_ALL}" "${TMP_PAGE}" <<'PY' import json, sys all_arr = json.load(open(sys.argv[1])) page = json.load(open(sys.argv[2])) all_arr.extend(page.get("transactions") or []) json.dump(all_arr, open(sys.argv[1], "w")) PY NEXT=$(python3 -c " import json, sys d = json.load(open(sys.argv[1])) m = d.get('meta') or {} print(m.get('next_page') or '') " "${TMP_PAGE}") rm -f "${TMP_PAGE}" [[ -z "${NEXT}" || "${NEXT}" == "None" ]] && break page=$((page+1)) done if [[ "${FMT}" == "json" ]]; then cat "${TMP_ALL}" exit 0 fi python3 - "${TMP_ALL}" "${SIDE}" "${SINCE}" "${UNTIL}" "${ACCOUNT}" <<'PY' import json, sys txs = json.load(open(sys.argv[1])) side_filter, since, until, account = sys.argv[2:6] if side_filter: txs = [t for t in txs if t.get("side") == side_filter] txs.sort(key=lambda t: t.get("settled_at") or "") print(f"# Qonto transactions on account {account[:8]}... ({since} → {until})") print() print(f"{'date':<10} {'side':<6} {'amount':>10} {'cur':<3} {'op':<14} {'label':<40}") print("-" * 100) total_credit = total_debit = 0.0 for t in txs: dt = (t.get("settled_at") or "")[:10] amt = float(t.get("amount") or 0) side = t.get("side") or "-" op = (t.get("operation_type") or "-")[:14] label = (t.get("label") or "-")[:40] cur = t.get("currency") or "-" print(f"{dt:<10} {side:<6} {amt:>10.2f} {cur:<3} {op:<14} {label:<40}") if side == "credit": total_credit += amt if side == "debit": total_debit += amt print("-" * 100) print(f"# {len(txs)} txn(s) — credit total: {total_credit:.2f}, debit total: {total_debit:.2f}, net: {total_credit - total_debit:+.2f}") PY