4.3 KiB
4.3 KiB
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:
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:
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
# 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
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
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
# 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:
- Short-term: Implement Option 2 (fetch payments) — works with existing data
- Long-term: Also populate
ref_clientwhen creating new invoices — reduces API calls
Testing Checklist
- Fetch payments for a known paid invoice
- Verify
num_paymentcontains GC payment ID - Run reconciliation with
--gc-payoutson 2025 data - Confirm
GC_NO_INVOICEcount drops from 59 to near-zero - Verify no false negatives (legitimate
GC_NO_INVOICEstill 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}/paymentsreturns array of payment records - Payment record field:
num_payment(string) — where--fixstores 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 asDOLIBARR_PAID_NO_GC