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>
65 lines
2.2 KiB
Ruby
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
|