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