diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cbc732c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,184 @@ +# 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 diff --git a/README.md b/README.md index 4524fcb..c62e91d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A standalone Ruby script that cross-checks three financial systems and flags discrepancies. +**Manual tool** — Export CSVs from GoCardless and Shine, run the script locally, review the report. No API integrations with payment providers, no background processes, no coupling to other projects. + ## The problem it solves Payments flow through three separate systems that are not automatically linked: