# GoCardless Payment ID Matching — Implementation Plan ## Problem Currently, 59 GC payments in 2025 are flagged as `GC_NO_INVOICE` even though they correspond to real Dolibarr invoices. The issue: matching relies on `description` field containing the invoice ref, but your GC payments use contract IDs like `REC-CT2408-0012` instead. **Solution:** Match by GoCardless payment ID (`PM014J7X4PY98T`) stored in Dolibarr. --- ## Dolibarr Storage Options ### Option 1: Invoice `ref_client` field Store GC payment ID on the invoice itself: ```ruby PUT /invoices/{id} { "ref_client": "PM014J7X4PY98T" } ``` **Pros:** - Single field, standard Dolibarr usage - Visible in invoice UI - No extra API calls needed **Cons:** - Requires write access when creating invoices - Need to update invoice creation workflow --- ### Option 2: Payment `num_payment` field (already in use) GC payment ID is already stored when `--fix` records a payment: ```ruby POST /invoices/paymentsdistributed { "num_payment": "PM014J7X4PY98T" } ``` **Pros:** - Already implemented in `fixer.rb` - Semantically correct (payment ID belongs on payment record) - No workflow changes needed **Cons:** - Requires fetching payment records per invoice (extra API calls) - More complex matching logic --- ## Recommended: Option 2 (Payment Record) Since `--fix` already stores `num_payment`, we just need to **fetch and check payment records**. ### Implementation Steps #### 1. Add payment fetching to `DolibarrFetcher` ```ruby # lib/reconciliation/dolibarr_fetcher.rb def fetch_payments_for_invoice(invoice_id) payments = @client.get("/invoices/#{invoice_id}/payments") || [] payments.map do |p| { id: p["id"], num_payment: p["num_payment"].to_s.strip, # GC payment ID if set date: parse_unix_date(p["datep"]), amount: p["amount"].to_f } end end ``` #### 2. Update `Invoice` struct to include GC payment IDs ```ruby Invoice = Struct.new( :id, :ref, # ... existing fields ... :gc_payment_ids, # Array of GC payment IDs from payment records keyword_init: true ) ``` #### 3. Fetch payments during invoice fetch ```ruby def fetch_invoices # ... existing code ... invoices = raw_invoices.map do |raw| inv = parse_invoice(raw, name_by_id) inv.gc_payment_ids = fetch_payments_for_invoice(inv.id).map { |p| p[:num_payment] }.compact inv end # ... rest of code ... end ``` #### 4. Update engine matching to check GC ID first ```ruby # lib/reconciliation/engine.rb def find_invoice(payment) # 0. GC ID match (strongest) — Dolibarr payment num_payment == GC payment id invoice = @invoices.find do |inv| inv.gc_payment_ids&.include?(payment.id) end return [invoice, :gc_id] if invoice # 1. Strong match: GC description == Dolibarr invoice ref # ... existing code ... end ``` --- ## API Call Impact | Current | After Change | |---------|--------------| | 1 call: `GET /invoices` | 1 call: `GET /invoices` | | 1 call: `GET /thirdparties` | 1 call: `GET /thirdparties` | | — | N calls: `GET /invoices/{id}/payments` (one per invoice) | For 100 invoices: ~102 API calls total (still well within rate limits) --- ## Alternative: Hybrid Approach If you want to future-proof: 1. **Short-term:** Implement Option 2 (fetch payments) — works with existing data 2. **Long-term:** Also populate `ref_client` when creating new invoices — reduces API calls --- ## Testing Checklist - [ ] Fetch payments for a known paid invoice - [ ] Verify `num_payment` contains GC payment ID - [ ] Run reconciliation with `--gc-payouts` on 2025 data - [ ] Confirm `GC_NO_INVOICE` count drops from 59 to near-zero - [ ] Verify no false negatives (legitimate `GC_NO_INVOICE` still flagged) --- ## Files to Modify | File | Changes | |------|---------| | `lib/reconciliation/dolibarr_fetcher.rb` | Add `fetch_payments_for_invoice`, update `Invoice` struct | | `lib/reconciliation/engine.rb` | Add GC ID match as first priority in `find_invoice` | | `README.md` | Document that `num_payment` must be set (via `--fix` or manually) | --- ## Notes - Dolibarr endpoint: `GET /invoices/{id}/payments` returns array of payment records - Payment record field: `num_payment` (string) — where `--fix` stores GC payment ID - Existing invoices already have this set if marked via `--fix` - Manually-paid invoices won't have `num_payment` — they'll correctly appear as `DOLIBARR_PAID_NO_GC`