# 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