Initial implementation of the reconciliation script
Standalone Ruby script reconciling GoCardless payments, Dolibarr invoices (via API), and Shine bank statements. Three-pass engine: GC↔Dolibarr matching, open invoice audit, payout↔bank verification. Includes dry-run and --fix mode to auto-mark Dolibarr invoices as paid. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
88
bin/reconcile
Executable file
88
bin/reconcile
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/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,
|
||||
shine: nil
|
||||
}
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = <<~BANNER
|
||||
Usage: bin/reconcile --from DATE --to DATE --gc 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("--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 << "--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])
|
||||
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,
|
||||
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
|
||||
Reference in New Issue
Block a user