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>
This commit is contained in:
64
lib/reconciliation/gocardless_parser.rb
Normal file
64
lib/reconciliation/gocardless_parser.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user