# Agent Instructions — Dolibarr / GoCardless / Shine Reconciliation ## Project Goal Reconcile invoices and payments across three financial systems: - **Dolibarr** — Invoice management (via REST API) - **GoCardless** — Direct debit payments (via CSV export) - **Shine** — Bank account (via CSV export) ## Design Principles 1. **Manual operation** — CSV-based, no background processes, no API integrations with payment providers 2. **Standalone** — No coupling to `console.cyanet.fr` or other projects 3. **Read-only by default** — `--fix` mode is opt-in and only affects invoices flagged `GC_PAID_DOLIBARR_OPEN` 4. **Audit trail** — CSV exports serve as snapshots; output CSVs written to `tmp/` ## Architecture ``` bin/reconcile # CLI entry point lib/ boot.rb # Dependency loader dolibarr/client.rb # HTTP client (HTTParty) reconciliation/ dolibarr_fetcher.rb # Fetch invoices + customer names gocardless_parser.rb # Parse payments CSV gocardless_payouts_parser.rb # Parse payouts CSV (fees) shine_parser.rb # Parse Shine bank CSV engine.rb # 3-pass matching logic reporter.rb # Terminal report + CSV export fixer.rb # Apply --fix mode ``` ## Data Flow ``` ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ GoCardless │ │ Dolibarr │ │ Shine │ │ payments │ │ invoices │ │ transactions│ │ CSV │ │ API │ │ CSV │ └──────┬──────┘ └──────┬───────┘ └──────┬──────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ Engine │ │ Pass 1: GC payments ↔ Dolibarr invoices │ │ Pass 2: Open Dolibarr invoice audit │ │ Pass 3: GC payouts ↔ Shine bank credits │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────┐ │ Reporter + Fixer │ │ Terminal report │ │ CSV export to tmp/ │ │ Optional --fix │ └────────────────────────┘ ``` ## Matching Logic ### Pass 1 — GC ↔ Dolibarr | Match Type | Criteria | |------------|----------| | **Strong** | `payment.description == invoice.ref` (case-insensitive) | | **Soft** | Same amount + customer name + date within ±7 days | ### Pass 2 — Open Invoice Audit All open Dolibarr invoices with no GC match → `DOLIBARR_OPEN_NO_GC` ### Pass 3 — GC Payouts ↔ Shine | Match Type | Criteria | |------------|----------| | **Strong** | Payout reference found in Shine `Libellé` | | **Fallback** | Exact net amount + date within ±2 days | ## Key Files to Modify | Task | File | |------|------| | Add new flag | `lib/reconciliation/engine.rb` (constants + Match struct) | | Change matching logic | `lib/reconciliation/engine.rb` (pass1/2/3 methods) | | Add parser field | `lib/reconciliation/*_parser.rb` (Payment/Transaction/Invoice struct) | | Change report output | `lib/reconciliation/reporter.rb` (print_summary, write_csv) | | Add CLI option | `bin/reconcile` (OptionParser + wiring) | | Dolibarr API calls | `lib/dolibarr/client.rb`, `lib/reconciliation/dolibarr_fetcher.rb` | ## Coding Conventions - **Ruby 3.x** — Use `frozen_string_literal: true` - **No external dependencies** — Only stdlib + HTTParty (already in Gemfile) - **Amounts in cents** — Always use integers to avoid float precision issues - **Date tolerance** — Use `DATE_TOLERANCE` / `PAYOUT_DATE_TOLERANCE` constants (configurable via ENV) - **Name normalization** — Strip accents, lowercase, sort words for comparison ## Testing Checklist Before committing changes: ```bash # Syntax check ruby -c lib/reconciliation/*.rb ruby -c bin/reconcile # Run reconciliation (dry run) ruby bin/reconcile --from 2026-01-01 --to 2026-01-31 \ --gc gocardless/*.csv --gc-payouts gocardless/*.csv \ --shine shine/*/*.csv # Verify CSV output cat tmp/reconciliation_*.csv cat tmp/payouts_fees_*.csv ``` ## Common Patterns ### Adding a new flag ```ruby # 1. Define constant in Engine NEW_FLAG = :new_flag # 2. Add to Match struct Match = Struct.new( :flag, :invoice, :payment, :match_type, :partial, :retry_group, :new_field, # if needed keyword_init: true ) # 3. Set flag in matching logic results << Match.new(flag: NEW_FLAG, ...) # 4. Handle in reporter r[:new_flag_matches].each do |m| puts " #{m.invoice.ref}" end ``` ### Adding a new parser field ```ruby # 1. Add to struct Payment = Struct.new( :id, :charge_date, :amount_cents, :new_field, keyword_init: true ) # 2. Parse from CSV Payment.new( id: row["id"].to_s.strip, new_field: row["new_column"].to_s.strip, ... ) # 3. Use in engine/reporter ``` ## Gotchas 1. **Dolibarr `status=1`** — Returns all non-draft invoices (open + paid + cancelled). Filter by `statut` field in response. 2. **Shine CSV format** — Semicolon separator, French decimal (`51,10`), Windows CRLF 3. **GoCardless fees** — Payout net amount < sum of payments (fee deducted). This is normal. 4. **Credit notes** — Negative `total_ttc` invoices are excluded from reconciliation 5. **Supplier invoices** — Separate API endpoint (`/supplierinvoices`), not handled ## Environment Variables | Variable | Default | Purpose | |----------|---------|---------| | `DOLIBARR_URL` | — | Dolibarr API base URL | | `DOLIBARR_API_KEY` | — | API authentication | | `DOLIBARR_GC_PAYMENT_ID` | — | Payment method ID for `--fix` | | `DOLIBARR_BANK_ACCOUNT_ID` | `1` | Bank account ID for `--fix` | | `RECONCILIATION_DATE_TOLERANCE` | `7` | Days for soft date matching | | `RECONCILIATION_PAYOUT_TOLERANCE` | `2` | Days for payout date matching | ## Related Files - `docs/reconciliation_plan.md` — Original design document - `docs/dolibarr.json` — Dolibarr API spec - `.env.example` — Environment variable template