Files
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

97 lines
2.7 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
require "optparse"
require "date"
require_relative "../lib/boot"
options = {
fix: false,
from: nil,
to: nil,
gc: nil,
gc_payouts: nil,
shine: nil
}
OptionParser.new do |opts|
opts.banner = <<~BANNER
Usage: bin/reconcile --from DATE --to DATE --gc PATH [--gc-payouts PATH] [--shine PATH] [--fix]
Options:
BANNER
opts.on("--from DATE", "Start date (YYYY-MM-DD), inclusive") do |v|
options[:from] = Date.parse(v)
end
opts.on("--to DATE", "End date (YYYY-MM-DD), inclusive") do |v|
options[:to] = Date.parse(v)
end
opts.on("--gc PATH", "GoCardless payments CSV file path") do |v|
options[:gc] = v
end
opts.on("--gc-payouts PATH", "GoCardless payouts CSV file path (enables exact fee reporting)") do |v|
options[:gc_payouts] = v
end
opts.on("--shine PATH", "Shine bank statement CSV file path (optional)") do |v|
options[:shine] = v
end
opts.on("--fix", "Apply fixes: mark matching Dolibarr invoices as paid via API") do
options[:fix] = true
end
opts.on("-h", "--help", "Show this help") do
puts opts
exit
end
end.parse!
# Validate required options
errors = []
errors << "--from is required" unless options[:from]
errors << "--to is required" unless options[:to]
errors << "--gc is required" unless options[:gc]
errors << "--gc file not found: #{options[:gc]}" if options[:gc] && !File.exist?(options[:gc])
errors << "--gc-payouts file not found: #{options[:gc_payouts]}" if options[:gc_payouts] && !File.exist?(options[:gc_payouts])
errors << "--shine file not found: #{options[:shine]}" if options[:shine] && !File.exist?(options[:shine])
unless errors.empty?
errors.each { |e| warn "Error: #{e}" }
exit 1
end
# Run reconciliation
client = Dolibarr::Client.new
fetcher = Reconciliation::DolibarrFetcher.new(client, from: options[:from], to: options[:to])
gc_data = Reconciliation::GocardlessParser.parse(options[:gc], from: options[:from], to: options[:to])
gc_payouts = options[:gc_payouts] ? Reconciliation::GocardlessPayoutsParser.parse(options[:gc_payouts]) : []
shine_data = options[:shine] ? Reconciliation::ShineParser.parse(options[:shine]) : []
dolibarr_invoices = fetcher.fetch_invoices
engine = Reconciliation::Engine.new(
dolibarr_invoices: dolibarr_invoices,
gc_payments: gc_data,
gc_payouts: gc_payouts,
shine_transactions: shine_data,
from: options[:from],
to: options[:to]
)
result = engine.run
reporter = Reconciliation::Reporter.new(result, from: options[:from], to: options[:to])
reporter.print_summary
if options[:fix]
fixer = Reconciliation::Fixer.new(client)
fixer.apply(result[:gc_paid_dolibarr_open])
end
reporter.write_csv