#!/usr/bin/env bash # List Wise activities (incoming + outgoing) for a period. # # Usage: # wise-transactions.sh [--since YYYY-MM-DD] [--until YYYY-MM-DD] # [--month YYYY-MM] # [--type TRANSFER|CARD_ORDER|BALANCE_CASHBACK|FEATURE_CHARGE|BALANCE_DEPOSIT|...] # [--status COMPLETED|CANCELLED|IN_PROGRESS|UPCOMING|REQUIRES_ATTENTION] # [--enrich] # also fetch /v1/transfers/{id} for the wire reference # [--json] # raw list, no table # # Backed by GET /v1/profiles/{WISE_PROFILE_ID}/activities — does NOT require # SCA and DOES expose incoming transfers (unlike /balance-statements which is # region-restricted for EU personal tokens, see SKILL.md). # # Pagination is followed automatically via the cursor. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BANK_CURL="${SCRIPT_DIR}/bank-curl.sh" SINCE=""; UNTIL=""; MONTH=""; TYPE=""; STATUS=""; ENRICH=0; FMT="table" while [[ $# -gt 0 ]]; do case "$1" in --since) SINCE="$2"; shift 2 ;; --until) UNTIL="$2"; shift 2 ;; --month) MONTH="$2"; shift 2 ;; --type) TYPE="$2"; shift 2 ;; --status) STATUS="$2"; shift 2 ;; --enrich) ENRICH=1; shift ;; --json) FMT="json"; shift ;; -h|--help) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "wise-transactions.sh: unknown arg: $1" >&2; exit 2 ;; esac done set -a; source "${SCRIPT_DIR}/../../dolibarr/.env"; set +a : "${WISE_PROFILE_ID:?wise-transactions.sh: WISE_PROFILE_ID not set in .env}" # Date handling — Wise wants full ISO 8601 with millis + Z if [[ -n "${MONTH}" ]]; then SINCE="${MONTH}-01" UNTIL="$(python3 -c "import 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=365)).strftime('%Y-%m-%d'))")" [[ -z "${UNTIL}" ]] && UNTIL="$(python3 -c "import datetime; print(datetime.date.today().strftime('%Y-%m-%d'))")" SINCE_ISO="${SINCE}T00:00:00.000Z" UNTIL_ISO="${UNTIL}T23:59:59.999Z" WORK="$(mktemp -d -t wiseact.XXXXXX)" trap 'rm -rf "${WORK}"' EXIT # Paginate echo '[]' > "${WORK}/all.json" CURSOR="" PAGE=1 while :; do Q="size=100&since=${SINCE_ISO}&until=${UNTIL_ISO}" [[ -n "${TYPE}" ]] && Q="${Q}&monetaryResourceType=${TYPE}" [[ -n "${STATUS}" ]] && Q="${Q}&status=${STATUS}" [[ -n "${CURSOR}" ]] && Q="${Q}&nextCursor=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "${CURSOR}")" TMP_PAGE=$(mktemp -t wisepg.XXXXXX.json) "${BANK_CURL}" wise "/v1/profiles/${WISE_PROFILE_ID}/activities?${Q}" > "${TMP_PAGE}" python3 - "${WORK}/all.json" "${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("activities") or []) json.dump(all_arr, open(sys.argv[1], "w")) PY CURSOR=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('cursor') or '')" "${TMP_PAGE}") rm -f "${TMP_PAGE}" if [[ -z "${CURSOR}" || "${CURSOR}" == "None" ]]; then break fi PAGE=$((PAGE+1)) done # Optional enrichment: for each TRANSFER, fetch /v1/transfers/{id} and merge `reference` if [[ "${ENRICH}" == "1" ]]; then python3 - "${WORK}/all.json" <<'PY' > "${WORK}/ids.txt" import json, sys acts = json.load(open(sys.argv[1])) for a in acts: r = a.get("resource") or {} if r.get("type") == "TRANSFER" and r.get("id"): print(r["id"]) PY > "${WORK}/refs.json" echo '{}' > "${WORK}/refs.json" while read -r tid; do [[ -z "${tid}" ]] && continue TMP_T=$(mktemp -t wiset.XXXXXX.json) if "${BANK_CURL}" wise "/v1/transfers/${tid}" > "${TMP_T}" 2>/dev/null; then python3 - "${WORK}/refs.json" "${TMP_T}" "${tid}" <<'PY' import json, sys refs = json.load(open(sys.argv[1])) t = json.load(open(sys.argv[2])) refs[sys.argv[3]] = t.get("reference") or "" json.dump(refs, open(sys.argv[1], "w")) PY fi rm -f "${TMP_T}" done < "${WORK}/ids.txt" fi if [[ "${FMT}" == "json" ]]; then cat "${WORK}/all.json" exit 0 fi python3 - "${WORK}/all.json" "${WORK}/refs.json" "${ENRICH}" "${SINCE}" "${UNTIL}" <<'PY' import json, sys, re, os acts_path, refs_path, enrich, since, until = sys.argv[1:6] acts = json.load(open(acts_path)) refs = json.load(open(refs_path)) if os.path.exists(refs_path) else {} def strip(s): return re.sub(r'<[^>]+>', '', s or '').strip() def parse_amount(s): # "+ 5,100.00 EUR" -> (+, 5100.00, EUR) s = strip(s) sign = "+" if s.startswith("+") else "-" m = re.search(r'([\d,.]+)\s*([A-Z]{3})', s) if not m: return sign, 0.0, "?" return sign, float(m.group(1).replace(",", "")), m.group(2) # Sort by date (ascending) acts.sort(key=lambda a: a.get("createdOn") or "") print(f"# Wise activities for profile (window {since} → {until})") print() header = f"{'date':<10} {'type':<18} {'status':<10} {'sign':<4} {'amount':>10} {'cur':<3} {'title':<28}" if int(enrich): header += " reference" print(header) print("-" * (len(header) + 30 if int(enrich) else len(header))) total_credit = total_debit = 0.0 for a in acts: dt = (a.get("createdOn") or "")[:10] sign, amt, cur = parse_amount(a.get("primaryAmount") or "") typ = a.get("type", "-") st = a.get("status", "-") title = strip(a.get("title") or "")[:28] line = f"{dt:<10} {typ:<18} {st:<10} {sign:<4} {amt:>10.2f} {cur:<3} {title:<28}" if int(enrich): r = a.get("resource") or {} ref = refs.get(str(r.get("id", "")), "") if r.get("type") == "TRANSFER" else "" line += f" {ref}" print(line) if sign == "+": total_credit += amt if sign == "-": total_debit += amt print("-" * (len(header) + 30 if int(enrich) else len(header))) print(f"# {len(acts)} activity(ies) — credit: +{total_credit:.2f}, debit: -{total_debit:.2f}, net: {total_credit - total_debit:+.2f}") PY