Three improvements that reduce the V6.1 exit-1 signal from 10 to 1 on
the current Arcodange baseline. Every bucket now has a single, clear
purpose; the only entry counted as a failure is a genuine action item.
A. fk_account context on dolibarr-only
- Fetches /bankaccounts and tags each dolibarr-only with the account
ref + label (e.g. "CCA1 (G.RADUREAU Compte Courant Asso)").
- Splits dolibarr-only into "on API-tracked accounts" (QON*/WIS* — real
gaps) vs "not in API scope" (CCA1 / personal — expected gaps).
- Personal-account entries no longer count toward the failure verdict.
B. Avoir-cycle netting
- Pairs AVC entries of -X on socid S with FAC entries of +X on the
same socid within ±5d.
- Both surface in a dedicated AVOIR-NETTED bucket and are excluded from
dolibarr-only, since the bank only sees the net of the cycle.
- Resolves the V6.1 noise where AVC001-CL0001001 + FAC001-CL00001
appeared as fake gaps for a 510€ cancel-and-reissue dance.
C. Wire-reference strong matching (--enrich flag, opt-in)
- When --enrich is passed, bank-match.sh fetches /v1/transfers/{id}
per Wise TRANSFER and reads the wire `reference` field.
- References containing a FAC\d+(CL\d+)? pattern strong-match against
the corresponding Dolibarr customer invoice (annotated [wire-ref]
vs the loose [amt+date] kind).
- Verified on FAC002 5100€: KM's wire memo "FOR INVOICE FAC002CL0001002"
gives an unambiguous match independent of date drift.
Baseline (Jan-May 2026, --enrich on):
6 matched · 1 internal · 2 avoir-netted · 7 bank-known · 1 bank-UNKNOWN
0 dol-only-API · 7 dol-only-personal
→ exit-1 count = 1 (just the +2147€ KM Wise 2026-05-29 to record).
The CLI (bin/arcodange) gains --enrich on the match subcommand. The
SKILL.md has a new "V7 bucket structure" section explaining the seven
buckets and a before/after table showing the signal/noise improvement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
name, description, requires
| name | description | requires | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| arcodange-bank-reco | 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). |
|
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 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:
-
/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 respondingx-2fa-approval-result: REJECTEDregardless. -
/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"<positive>+ 5,100.00 EUR</positive>"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: <login>:<secret_key>header./v2/organizationlists bank accounts + balances./v2/transactions?bank_account_id=<id>&settled_at_from=<iso>&settled_at_to=<iso>lists transactions with pagination viacurrent_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
- Base skill set up (dolibarr/README.md).
.envextended with:QONTO_LOGIN=arcodange-XXXXX QONTO_SECRET_KEY=<secret> QONTO_ORG_SLUG=arcodange-XXXXX WISE_API_TOKEN=<token> WISE_PROFILE_ID=<numeric id of the BUSINESS profile>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
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
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. The full Jan-May 2026 view shows 9 transactions netting to +4191.54 € — exactly the current Qonto balance.
3. Wise activities
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. 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)
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 — 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
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.
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)
Bank movements that have no Dolibarr counterpart fall into two groups:
- 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.
- 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 for current entries):
{
"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:
- Run
bin/arcodange bank match→ look at BANK-ONLY unknown. - For each recurring entry that's "expected", add a pattern to
known-patterns.json. - Re-run match → the entry should now appear in the known sub-bucket.
- 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_chargesMISTRAL.AI/CLAUDE.AI→ ai_subscriptionWise *Plan→ bank_fee (Wise account plan billed via Qonto card)qonto_fee→ bank_feeBALANCE_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.
V7 bucket structure
V7 adds three improvements that reshape the output buckets:
| Bucket | Meaning | Counts toward exit-1? |
|---|---|---|
| MATCHED | Bank ↔ Dolibarr paired. Annotated with match kind: [wire-ref] (strong, via --enrich) or [amt+date] (loose). |
No |
| INTERNAL | Wise↔Qonto consolidations (5000€ moved between Arcodange's own accounts). | No |
| AVOIR-NETTED | Dolibarr AVC + FAC cancellation cycles paired and excluded (the bank only saw the net). | No |
| BANK-ONLY — known patterns | Bank movement with a known-patterns.json annotation. Intentional gap. |
No |
| BANK-ONLY — unknown | Bank movement with no Dolibarr counterpart AND no catalog pattern. Real action item. | Yes |
| DOLIBARR-ONLY — on API-tracked accounts (QON*/WIS*) | Dolibarr payment that the bank should have shown. Real gap. | Yes |
| DOLIBARR-ONLY — not in API scope (CCA1 perso etc.) | Expected gap — we have no API on those accounts. | No |
Exit code 0 iff the two "real gap" buckets are empty.
--enrich — wire-reference strong matching
bank-match.sh --enrich fetches /v1/transfers/{id} for each Wise TRANSFER and reads the reference field (the wire memo from the sender, e.g. FROM KISSMETRICS HOLDINGS INC FOR INVOICE FAC002CL0001002/ VENDOR:DEV). When the reference contains a FAC\d+(CL\d+)? pattern matching a Dolibarr customer invoice, that pairing takes precedence over the loose date+amount match. Only the strong-matched ones get [wire-ref]; the rest fall through to [amt+date]. Cost: 1 extra HTTP call per Wise transfer.
Avoir cycle netting
When Arcodange cancels and reissues an invoice (FAC001 → AVC001 + FAC001-NEW), the bank sees one net credit but Dolibarr stores 3 payment entries. V7 pairs AVC entries of -X with FAC entries of +X for the same socid within ±5d, surfaces them in AVOIR-NETTED, and excludes them from dolibarr-only. Removes the V6.1 noise where AVC001 + FAC001-CL00001 appeared as fake gaps.
fk_account context
bank-match.sh now fetches /bankaccounts and tags dolibarr-only entries with their account ref + label. Splits into API-tracked (QON*/WIS* — real gaps) vs not-in-scope (everything else — expected). The 7 CCA1 personal-account entries that used to look like failures are now correctly classified as expected gaps.
Effect on the baseline
| V6 | V6.1 | V7 | |
|---|---|---|---|
| MATCHED | 6 (all amt+date) | 6 | 6 (1 wire-ref strong + 5 amt+date when --enrich) |
| BANK-ONLY total | 8 mixed | 7 known + 1 UNKNOWN | 7 known + 1 UNKNOWN |
| AVOIR-NETTED | — | — | 2 (silently absorbed) |
| DOL-only TRUE GAP | 9 (noisy) | 9 (noisy) | 0 |
| DOL-only EXPECTED | — | — | 7 (CCA1 personal) |
| Exit-1 signal count | 17 (noise) | 10 (less noise) | 1 (just the +2147€ KM) |
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
--enrichwire reference: search the Wise reference text forFAC\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.