Files
dolibarr_shine_reconciliation/lib/reconciliation/shine_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.0 KiB
Ruby

# frozen_string_literal: true
require "csv"
require "date"
module Reconciliation
class ShineParser
Transaction = Struct.new(
:id,
:date,
:credit_cents, # Integer, > 0 for incoming money
:debit_cents, # Integer, > 0 for outgoing money
:label, # Libellé
:counterparty, # Nom de la contrepartie
keyword_init: true
)
def self.parse(csv_path)
content = File.read(csv_path, encoding: "UTF-8")
# Normalise Windows CRLF
content = content.gsub("\r\n", "\n").gsub("\r", "\n")
rows = CSV.parse(content, headers: true, col_sep: ";")
transactions = rows.filter_map do |row|
date = safe_parse_date(row["Date de la valeur"])
next unless date
Transaction.new(
id: row["Transaction ID"].to_s.strip,
date: date,
credit_cents: parse_french_amount(row["Crédit"]),
debit_cents: parse_french_amount(row["Débit"]),
label: row["Libellé"].to_s.strip,
counterparty: row["Nom de la contrepartie"].to_s.strip
)
end
$stderr.puts "[ShineParser] Loaded #{transactions.size} transactions from #{csv_path}"
transactions
end
# Returns all GoCardless credit transactions (incoming payouts from GC)
def self.gocardless_credits(transactions)
transactions.select do |t|
t.credit_cents > 0 &&
t.counterparty.upcase.include?("GOCARDLESS")
end
end
private_class_method def self.safe_parse_date(str)
return nil if str.nil? || str.strip.empty?
Date.strptime(str.strip, "%d/%m/%Y")
rescue Date::Error, ArgumentError
nil
end
# French decimal format: "51,10" → 5110 (cents)
private_class_method def self.parse_french_amount(str)
return 0 if str.nil? || str.strip.empty?
(str.strip.gsub(/\s/, "").gsub(",", ".").to_f * 100).round
end
end
end