DEV Community

Golden Alien
Golden Alien

Posted on

🛠️ merge_payment_ledger: Merge Gumroad, Stripe, and PayPal CSVs into a unified ledger for easy

Merge Payment Ledger

Merge Payment Ledger is a lightweight Python tool designed to consolidate transaction data from Gumroad, Stripe, and PayPal into a single, standardized CSV ledger. This is especially useful for creators and small businesses that use multiple platforms to sell digital products or services and want a unified view of their revenue.

Features

  • Automatically detects and parses CSV files from Gumroad, Stripe, and PayPal
  • Normalizes diverse column names and date formats into a consistent schema
  • Outputs a clean, unified ledger with essential fields: date, platform, transaction_id, description, amount (USD), fee, net, and buyer info
  • Handles currency conversion warnings (all inputs assumed USD)
  • Simple CLI interface for easy automation

Usage

Run the script from the command line:

python merge_payment_ledger.py -i gumroad.csv stripe.csv paypal.csv -o ledger.csv
Enter fullscreen mode Exit fullscreen mode

You can pass multiple input files from any supported provider. The tool identifies the source platform by analyzing header patterns.

Output

The resulting ledger.csv contains a unified timeline of all transactions sorted by date, making it ideal for accounting, tax preparation, or dashboarding in tools like Google Sheets or Excel.

Requirements

  • Python 3.7+
  • Standard library only — no external dependencies

Limitations

  • Assumes all transactions are in USD
  • Limited to CSV exports using standard formats from each platform
  • Does not support refunds or disputes as separate line items (treated as negative net)

License

MIT

import csv
import argparse
import sys
from datetime import datetime
from typing import List, Dict, Optional


def detect_platform(headers: List[str]) -> Optional[str]:
    headers = [h.lower() for h in headers]
    if 'sale id' in headers and 'product name' in headers:
        return 'Gumroad'
    if 'transaction id' in headers and 'net' in headers and 'stripe fee' in headers:
        return 'Stripe'
    if 'transaction id' in headers and 'gross' in headers and 'fee' in headers and 'paypal' in headers:
        return 'PayPal'
    return None


def parse_row(platform: str, row: Dict[str, str]) -> Dict[str, str]:
    common = {
        'date': '',
        'platform': platform,
        'transaction_id': '',
        'description': '',
        'amount_usd': '',
        'fee': '',
        'net': '',
        'buyer': ''
    }
    try:
        if platform == 'Gumroad':
            common['date'] = row['Sale creation date']
            common['transaction_id'] = row['Sale ID']
            common['description'] = row['Product name']
            common['amount_usd'] = row['Price (including tax)']
            common['fee'] = str(float(row['Gumroad fee']) + float(row.get('Additional fee', '0')))
            common['net'] = row['Net']
            common['buyer'] = row['Customer email']
        elif platform == 'Stripe':
            common['date'] = row['Created (UTC)']
            common['transaction_id'] = row['Transaction ID']
            common['description'] = row['Description']
            common['amount_usd'] = row['Gross']
            common['fee'] = row['Stripe fee']
            common['net'] = row['Net']
            common['buyer'] = row.get('Customer email', '')
        elif platform == 'PayPal':
            common['date'] = row['Date']
            common['transaction_id'] = row['Transaction ID']
            common['description'] = row['Subject']
            common['amount_usd'] = row['Gross']
            common['fee'] = row['Fee']
            common['net'] = row['Net']
            common['buyer'] = row['Name']
    except KeyError as e:
        print(f"Missing column: {e}", file=sys.stderr)
    return common


def process_file(filepath: str, all_rows: List[Dict[str, str]]):
    with open(filepath, mode='r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        platform = detect_platform(reader.fieldnames)
        if not platform:
            print(f"Unknown format: {filepath}", file=sys.stderr)
            return
        for row in reader:
            if row.get(list(row.keys())[0]):  # skip empty rows
                all_rows.append(parse_row(platform, row))


def main():
    parser = argparse.ArgumentParser(description='Merge Gumroad, Stripe, PayPal CSVs into one ledger.')
    parser.add_argument('-i', '--inputs', nargs='+', required=True)
    parser.add_argument('-o', '--output', required=True)
    args = parser.parse_args()

    rows = []
    for file in args.inputs:
        process_file(file, rows)

    # Sort by date
    def parse_date(row):
        try:
            return datetime.strptime(row['date'].split()[0], '%Y-%m-%d')
        except:
            return datetime.min
    rows.sort(key=parse_date)

    # Write unified CSV
    if rows:
        with open(args.output, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=rows[0].keys())
            writer.writeheader()
            writer.writerows(rows)
        print(f"Wrote {len(rows)} transactions to {args.output}")
    else:
        print("No valid data found.")

if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)