---
name: arcodange-bank-reco
description: Bank-side reconciliation for Arcodange — cross-check Dolibarr customer + supplier payments against the actual movements on Qonto (FR business account, full API access) and Wise (BUSINESS EUR balance, activity-list API). Five workflows — (1) probe / discover auth + IDs; (2) list Qonto transactions for a period; (3) list Wise activities including incoming KissMetrics payments (via /v1/profiles/{pid}/activities — bypasses the EU statement endpoint restriction); (4) match bank movements against Dolibarr payments in three buckets (matched, bank-only, dolibarr-only) with auto-detection of Wise↔Qonto internal consolidations; (5) live balances per account with Dolibarr cross-check per fk_account. Surfaces concrete findings — incoming KM payments not yet entered in Dolibarr, expenses on the bank without supplier invoices recorded, date drift between bank settlement and Dolibarr saisie, and personal-account fk_account=3 movements that are invisible via API. Use when the user asks "réconcilier la banque", "qu'est-ce que la banque a vu que Dolibarr n'a pas", "match bank vs ERP", "audit comptable Arcodange", "did KM actually pay X", "cohort review bank evidence". Depends on `dolibarr` for the ERP side. SKIP for write operations (this is read-only; entries go through Dolibarr UI), for non-Arcodange bank accounts, and for Wise BUSINESS balance-statement endpoints (not available to EU personal tokens — we use the activity list instead).
requires:
bins: ["curl", "jq", "python3", "openssl"]
auth: true
---
# arcodange-bank-reco — close the loop between Dolibarr and the bank
The V1-V5 skills tell you what Dolibarr *thinks* happened. This one tells you what the **bank actually saw**, and matches the two sides. The three buckets it produces (matched / bank-only / dolibarr-only) are the foundation for any clean accounting audit and any cohort-review evidence pack.
Depends on the [dolibarr](../dolibarr/SKILL.md) base skill.
**CLI shortcuts:** `bin/arcodange bank probe | qonto-transactions | wise-transactions | match | balance | curl`
## Wise SCA & the EU restriction — the path we settled on
Wise has TWO ways to list movements:
1. **`/v1/profiles/{pid}/balance-statements/{balanceId}/statement.json`** — the obvious "statement" endpoint. **Returns 403 for EU personal tokens** (FR included). Wise's own docs say it: "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." Even with full SCA setup (RSA keypair + uploaded public key), the BUSINESS profile statements stay 403. Don't go down that rabbit hole — we did, and Wise just keeps responding `x-2fa-approval-result: REJECTED` regardless.
2. **`/v1/profiles/{pid}/activities`** — the activity list. **Works for EU personal tokens with no SCA.** Returns incoming + outgoing in a unified HTML-tagged feed. This is what the skill uses. The cost is that amounts come back as `"+ 5,100.00 EUR"` strings instead of structured numerics, so we parse HTML out — easy.
We tried the SCA path first, didn't work, then found this. Documented so the next operator doesn't repeat the dance.
## Qonto — no surprises
Qonto's API works as documented:
- `Authorization: :` header.
- `/v2/organization` lists bank accounts + balances.
- `/v2/transactions?bank_account_id=&settled_at_from=&settled_at_to=` lists transactions with pagination via `current_page`.
Wise↔Qonto integration in the Qonto UI does NOT expose Wise data via Qonto's API. We confirmed: no `/v2/external_accounts`, no `/v2/aggregated_accounts` endpoint surfaces Wise transactions. The two banks remain separately queried, then merged in the matching layer.
## Prerequisites
1. Base skill set up ([dolibarr/README.md](../dolibarr/README.md)).
2. `.env` extended with:
```
QONTO_LOGIN=arcodange-XXXXX
QONTO_SECRET_KEY=
QONTO_ORG_SLUG=arcodange-XXXXX
WISE_API_TOKEN=
WISE_PROFILE_ID=
```
3. `chmod 600 ~/.config/arcodange-erp/.env`; propagate to the two in-repo hard copies.
To generate the tokens:
- **Qonto**: https://app.qonto.com/ → Settings → Integrations → API → Generate a new key. Copy login + secret (shown once).
- **Wise**: https://wise.com/your-account/integrations-and-tools/api-tokens → Add new token → name it `arcodange-bank-reco-readonly` → scope read-only.
The `WISE_SCA_KEY_PATH` variable exists in the `.env` schema for completeness but is **NOT REQUIRED** today — the activity-list endpoint we use doesn't need SCA. Keep the keypair generated under `~/.config/arcodange-erp/wise-sca-*.pem` so we can revisit if Wise ever opens the statement endpoint to EU tokens.
## Workflows
### 1. Probe / discovery
```bash
bin/arcodange bank probe
```
Confirms auth on both banks and prints discovered IDs (Qonto org slug, Wise profile id, balance ids). Run once after any token rotation.
### 2. Qonto transactions
```bash
bin/arcodange bank qonto-transactions # last 90 days
bin/arcodange bank qonto-transactions --month 2026-03
bin/arcodange bank qonto-transactions --since 2026-01-01 --until 2026-05-31
bin/arcodange bank qonto-transactions --side credit # filter incoming only
```
Captured at [examples/qonto-transactions.txt](examples/qonto-transactions.txt). The full Jan-May 2026 view shows 9 transactions netting to +4191.54 € — exactly the current Qonto balance.
### 3. Wise activities
```bash
bin/arcodange bank wise-transactions # last 365 days
bin/arcodange bank wise-transactions --month 2026-03
bin/arcodange bank wise-transactions --type TRANSFER # filter
bin/arcodange bank wise-transactions --since 2026-01-01 --enrich # add wire references
```
Captured at [examples/wise-transactions.txt](examples/wise-transactions.txt). With `--enrich`, each TRANSFER is annotated with its wire reference (e.g. `FROM KISSMETRICS HOLDINGS INC FOR INVOICE FAC002CL0001002/ VENDOR:DEV`), which makes manual cross-checking trivial.
Net = +5308.25 € over the whole period, matches the live balance.
### 4. Bank ↔ Dolibarr match (the headline)
```bash
bin/arcodange bank match --month 2026-03 # one month
bin/arcodange bank match --since 2026-01-01 --until 2026-05-31
bin/arcodange bank match --month 2026-03 --window-days 14 # looser date tolerance
bin/arcodange bank match --include-fees # include cashback / charges in matching
```
Exit 0 if every bank movement and every Dolibarr payment in the window pair up cleanly; exit 1 otherwise (with the unmatched entries surfaced).
Captured at [examples/bank-match-2026-01-to-05.txt](examples/bank-match-2026-01-to-05.txt) — the V1 baseline.
**Output buckets:**
- `MATCHED` — bank ↔ Dolibarr, with the date delta (`Δ+6d`) so you can see drift between bank settlement and Dolibarr saisie.
- `INTERNAL` — Wise↔Qonto consolidations auto-detected by equal-amount opposite-sign on close dates. Excluded from matching against Dolibarr (they're transfers between Arcodange's own accounts, not external operations).
- `BANK-ONLY` — bank movements with no Dolibarr counterpart. Each one is either (a) a missing supplier invoice or unrecorded incoming payment, or (b) a Wise platform fee / cashback that doesn't translate to a Dolibarr entry.
- `DOLIBARR-ONLY` — Dolibarr payments without a bank movement. Usually fk_account=3 (the CCA1 personal account, not API-visible) or the cancel-and-reissue avoir cycle (bank sees the net, Dolibarr sees the three-way breakdown).
**Known findings from the V1 baseline** (to raise during cohort review):
- **Wise 2026-05-29 +2147 € from Kissmetrics NOT in Dolibarr** — M4 invoice probably emitted but the payment hasn't been entered. Action: enter the payment in Dolibarr.
- **+1000 € FOUREZ Quentin on Qonto 2026-01-21** — unknown income, ask Gabriel.
- **MISTRAL.AI -172.68 €, CLAUDE.AI -180 €, URSSAF -493 € on Qonto** — bank-only expenses, missing supplier invoices.
- **fk_account=3 supplier payments** (~430 € cumul) — paid from G.RADUREAU CCA personal account, not visible via Qonto/Wise APIs. This is normal; documented gap.
### 5. Live balances
```bash
bin/arcodange bank balance
```
Prints live balances per bank + the Dolibarr-side cumulative-payments-per-fk_account for cross-reference. Captured at [examples/bank-balance.txt](examples/bank-balance.txt).
Current state (V1 baseline):
- Qonto **Compte principal** : 4 191,54 € live
- Wise **STANDARD EUR** : 5 308,25 € live
- **Total bank-side** : 9 499,79 €
## Known-patterns catalog ([known-patterns.json](known-patterns.json))
Bank movements that have no Dolibarr counterpart fall into two groups:
1. **Intentional gaps** — operational expenses or one-off events the operator knows about (URSSAF mensuel, AI subs, capital deposit, Wise plan fees). These keep recurring but their accounting treatment is well-understood.
2. **Real action items** — incoming payments not yet entered, expenses missing a supplier invoice, anomalies.
Without a catalog, both look identical in the BANK-ONLY bucket — noise drowns the signal. The catalog is an operator-curated list of patterns; `bank-match.sh` reads it and splits BANK-ONLY into two sub-buckets:
- **BANK-ONLY — known patterns** : annotated with `[classification]` + a one-line note (which Dolibarr account to use, etc.). Don't action; just verify.
- **BANK-ONLY — unknown** : the real signal. Each entry is either a missing supplier invoice, an unrecorded payment, or a new pattern to add to the catalog.
**Schema** (JSON, see [known-patterns.json](known-patterns.json) for current entries):
```json
{
"patterns": [
{
"pattern": "regex (case-insensitive, matched against bank label + operation type)",
"classification": "capital_deposit | social_charges | ai_subscription | bank_fee | internal_topup | personal_apport | needs_classification",
"bank": "qonto | wise (optional, default both)",
"side": "credit | debit (optional, default both)",
"amount_min": 0.0, "amount_max": 99999.0, // optional numeric bounds
"note": "human-readable context — what this is, which Dolibarr account, recurring schedule, etc."
}
]
}
```
**Editing workflow:**
1. Run `bin/arcodange bank match` → look at BANK-ONLY unknown.
2. For each recurring entry that's "expected", add a pattern to `known-patterns.json`.
3. Re-run match → the entry should now appear in the known sub-bucket.
4. For one-off action items (e.g. "+2147 € KM May 29 not in Dolibarr"), don't add a pattern — enter it in Dolibarr instead.
**V6.1 catalog** ships with these patterns for the current Arcodange baseline:
- `FOUREZ Quentin` → capital_deposit (initial 1000 € apport via notaire, 2026-01-21)
- `URSSAF` → social_charges
- `MISTRAL.AI` / `CLAUDE.AI` → ai_subscription
- `Wise *Plan` → bank_fee (Wise account plan billed via Qonto card)
- `qonto_fee` → bank_fee
- `BALANCE_DEPOSIT|For your account plan` → internal_topup (the Wise +50/-50 self-funding pair)
After applying the catalog to the V6 baseline, the **only remaining BANK-UNKNOWN** is the **+2147 € KissMetrics payment on 2026-05-29** that hasn't been entered in Dolibarr — the actual signal.
## Matching heuristic — what's in v1 and what's V7
Today's match logic:
- **Amount equality** within 0.01 € (absolute value).
- **Date proximity** within `--window-days` (default 7 — covers most settlement drift).
- **Direction-aware** (bank credit ↔ Dolibarr customer payment; bank debit ↔ Dolibarr supplier payment).
- **Smallest date delta wins** when multiple candidates qualify.
- **Internal consolidation detection** by equal-amount opposite-sign cross-bank within ±3d.
V7 improvements to consider:
- **Reference-based matching** using the `--enrich` wire reference: search the Wise reference text for `FAC\d+` patterns and match by ref string. Stronger than date+amount when wire ref is informative.
- **Multi-row aggregation** for Dolibarr sub-payments: if invoice FAFXXX has two sub-payments on different dates summing to one bank movement, aggregate Dolibarr-side before matching.
- **Avoir cycle handling**: the V1 AVC001/FAC001-CL00001/FAC001-CL0001001 dance produces 3 Dolibarr-only rows for 1 bank credit. A smarter matcher would net the AVOIR before matching.
These are deliberately deferred — V1's accuracy is "good enough to surface the real issues" and the simple heuristic is easy to reason about.
## Out of scope
- **Writes**: the API tokens are read-only; payment recording happens in Dolibarr UI.
- **Wise BUSINESS balance statements via API** — region-blocked for EU personal tokens (documented above).
- **Wise transactions routed through Qonto** — Qonto's API doesn't expose them; the integration is UI-only.
- **fk_account=3 (CCA1 personal account)** — not API-accessible. Movements there must be reconciled manually against personal bank statements.
- **Bank statement export to CSV** — possible V8 fallback if any of the APIs go away; out of scope today.
- **Currency conversion** — everything is EUR. Multi-currency would need adapting the amount parser.