Files
dolibarr_shine_reconciliation/lib/reconciliation/gocardless_payouts_parser.rb
Kevin Bataille 0a048628a5 Add advanced reconciliation features: retry detection, partial payments, credit notes
New features:
- Credit note handling: Exclude negative invoices from GC reconciliation
- Retry detection: Flag invoices with multiple GC payment attempts
- Partial payment detection: Track invoices where remain_to_pay > 0
- Payout fee CSV: Export detailed fee breakdown per payout
- Configurable tolerances: RECONCILIATION_DATE_TOLERANCE, RECONCILIATION_PAYOUT_TOLERANCE

Files:
- lib/reconciliation/gocardless_payouts_parser.rb (new) - Parse GC payouts CSV
- lib/reconciliation/engine.rb - Add retry_group, partial fields to Match struct
- lib/reconciliation/reporter.rb - Show partial/retry in report, write payouts CSV
- lib/reconciliation/dolibarr_fetcher.rb - Add is_credit_note field, filter negatives
- bin/reconcile - Wire up --gc-payouts argument
- README.md - Document new features and --gc-payouts usage
- .env.example - Add optional tolerance settings

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-26 00:38:41 +01:00

65 lines
2.2 KiB
Ruby

# frozen_string_literal: true
require "csv"
require "date"
module Reconciliation
class GocardlessPayoutsParser
Payout = Struct.new(
:id,
:amount_cents, # Net amount transferred to bank (after all fees)
:total_payment_cents, # Gross sum of underlying payments (before fees)
:fee_cents, # Total fee deducted (total_payment - amount)
:transaction_fee_cents, # Per-transaction fee component
:surcharge_fee_cents, # Surcharge fee component
:tax_cents, # Tax on fees (informational, already in total fee)
:reference, # e.g. "CYANET-DKV4KN8FTM2" — appears in Shine Libellé
:status,
:arrival_date,
keyword_init: true
)
def self.parse(csv_path)
rows = CSV.read(csv_path, headers: true, encoding: "UTF-8")
payouts = rows.filter_map do |row|
next if row["status"] != "paid"
arrival_date = safe_parse_date(row["arrival_date"])
next unless arrival_date
amount_cents = to_cents(row["amount"])
total_payment_cents = to_cents(row["total_payment_amount"])
Payout.new(
id: row["id"].to_s.strip,
amount_cents: amount_cents,
total_payment_cents: total_payment_cents,
fee_cents: total_payment_cents - amount_cents,
transaction_fee_cents: to_cents(row["transaction_fee_debit"]),
surcharge_fee_cents: to_cents(row["surcharge_fee_debit"]),
tax_cents: to_cents(row["tax_debit"]),
reference: row["reference"].to_s.strip,
status: row["status"].to_s.strip,
arrival_date: arrival_date
)
end
$stderr.puts "[GocardlessPayoutsParser] Loaded #{payouts.size} paid payouts from #{csv_path}"
payouts
end
private_class_method def self.to_cents(str)
return 0 if str.nil? || str.strip.empty?
(str.strip.to_f * 100).round
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
end
end