Files
dolibarr_shine_reconciliation/lib/reconciliation/gocardless_parser.rb
Kevin Bataille 4decb3cb3c Initial implementation of the reconciliation script
Standalone Ruby script reconciling GoCardless payments, Dolibarr
invoices (via API), and Shine bank statements. Three-pass engine:
GC↔Dolibarr matching, open invoice audit, payout↔bank verification.
Includes dry-run and --fix mode to auto-mark Dolibarr invoices as paid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:23:07 +01:00

65 lines
2.2 KiB
Ruby

# frozen_string_literal: true
require "csv"
require "date"
module Reconciliation
class GocardlessParser
Payment = Struct.new(
:id, # e.g. "PM014J7X4PY98T"
:charge_date, # Date
:amount_cents, # Integer (euros * 100)
:description, # Free text — often the Dolibarr invoice ref
:status, # "paid_out", "failed", "cancelled", etc.
:payout_id, # e.g. "PO0010R1ARR0QZ"
:payout_date, # Date or nil
:customer_name, # "Jean DUPONT"
:customer_email,
keyword_init: true
)
# Statuses that represent a successful collection from the customer.
COLLECTED_STATUSES = %w[paid_out confirmed].freeze
FAILED_STATUSES = %w[failed].freeze
def self.parse(csv_path, from: nil, to: nil)
rows = CSV.read(csv_path, headers: true, encoding: "UTF-8")
payments = rows.filter_map do |row|
charge_date = safe_parse_date(row["charge_date"])
next if charge_date.nil?
next if from && charge_date < from
next if to && charge_date > to
payout_date = safe_parse_date(row["payout_date"])
Payment.new(
id: row["id"].to_s.strip,
charge_date: charge_date,
amount_cents: (row["amount"].to_f * 100).round,
description: row["description"].to_s.strip,
status: row["status"].to_s.strip,
payout_id: row["links.payout"].to_s.strip.then { |v| v.empty? ? nil : v },
payout_date: payout_date,
customer_name: build_name(row["customers.given_name"], row["customers.family_name"]),
customer_email: row["customers.email"].to_s.strip
)
end
$stderr.puts "[GocardlessParser] Loaded #{payments.size} payments from #{csv_path}"
payments
end
private_class_method def self.safe_parse_date(str)
return nil if str.nil? || str.strip.empty?
Date.parse(str.strip)
rescue Date::Error, ArgumentError
nil
end
private_class_method def self.build_name(given, family)
[given.to_s.strip, family.to_s.strip].reject(&:empty?).join(" ")
end
end
end