Reconciled logo Reconciled

Dev Notes

Architecture, data model, and assorted notes for anyone (mostly future me) poking around the code.

Tech Stack

LayerTechnology
LanguageJava 25
UISwing + FlatLaf 3.5.4
BuildGradle (Kotlin DSL)
SerializationGson 2.11.0 — plain JSON on disk, no database
PDF GenerationiText 8
Excel ExportPython 3 + openpyxl, auto-installed into a local venv on first export
Packagingjpackage — macOS DMG and Windows EXE, each built on its own platform (no cross-compiling installers)

Package Structure

Everything lives under com.github.shanebeee.reconciled:

reconciled/
├── Main.java          # Entry point, onboarding gate, profile resolution
├── model/             # Plain data classes (Boss, Invoice, CcaAsset, etc.)
├── storage/            # DataStorage (Preferences + paths), ProfileManager
├── util/               # Exporters, PDF/Excel generation, deduction math
└── view/               # Swing panels — one per major feature area

Data Storage

No database — everything is plain JSON on disk, which means it's human-readable, diffable, and trivially portable. If it's stored in iCloud Drive, sync is free.

Reconciled/
├── profiles.json            # id, name, dataPath, avatarColor per profile
├── Shane_Bolenback/          # one named subfolder per user profile
│   ├── settings/
│   │   ├── employee.json
│   │   ├── bosses.json
│   │   ├── settings.json
│   │   ├── expense_categories_{year}.json
│   │   ├── tax_brackets_{year}.json
│   │   └── cca_assets.json
│   ├── logs/yyyy-MM.json
│   ├── invoices/{invoice_log.json, *.pdf}
│   ├── receipts/{year}/{month}/
│   └── km/{year}/{trips.json, odometer.json}
└── Jenny_Francisco/           # completely independent from the above
    └── ...

The root directory itself defaults to ~/Library/Mobile Documents/com~apple~CloudDocs/Reconciled/ on iCloud, or ~/Reconciled/ locally, but is fully overridable during onboarding.

Preferences & Root Directory Resolution

The saved data path lives in macOS Preferences, under the node derived from com.github.shanebeee.reconciled.storage (backed by ~/Library/Preferences/com.github.shanebeee.plist when running as a packaged jpackage app — a plain ./gradlew run JVM instead falls under the generic com.apple.java.util.prefs.plist, which is worth knowing if you ever go looking for it in the wrong plist, like I did).

ProfileManager.getRootDir() is the single source of truth for where profiles.json lives. It checks, in order: an explicitly saved rootDirectory preference, then a legacy fallback to the saved data directory, then a hardcoded default. Once a root directory is saved, it's used permanently — Main.java never re-derives it from scratch on a later launch.

Why that last point matters: an earlier version of Main.java re-derived the root directory on every launch by string-matching the app's folder name inside the current data path, then truncating there. It worked fine — until a rename made that string match fail once. The fallback for a failed match was "just use the current (possibly deep) data path as the root," which meant the very next launch nested a new profile folder inside that path, then the launch after that nested another one inside that — compounding one level deeper every single time the app opened, with no error and no obvious symptom beyond "why do I have a profile called Me/Me/Me/Me/Me/Me/Me/Me now." Storing the resolved root once and trusting it thereafter fixed it for good.

CCA / Depreciation Math

CcaAsset is a pure computed model — there's no mutable running balance for Undepreciated Capital Cost (UCC). Opening UCC, the year's deduction, and Closing UCC are all derived mathematically from the asset's original cost, purchase date, and CRA class rate, for any requested tax year. That makes it trivial to preview a future or past year without needing to replay history.

CRA's half-year rule, as implemented:

Tax Set-Aside Calculation

Triggered when an invoice is marked Paid. Reconciled takes your year-to-date paid invoices, annualizes that figure (extrapolates a full-year estimate from what's been earned so far), and runs it against your configured federal and BC tax brackets plus CPP settings to estimate what to set aside for GST, federal tax, provincial tax, and CPP. Brackets are stored per tax year in tax_brackets_{year}.json, so historical years stay accurate even as rates change — deliberately kept as manual configuration rather than scraped from anywhere, since CRA rates are only published once a year anyway.

Naming History

Reconciled was originally called EmployeeTimesheet, built purely to log hours and generate invoices. As expense tracking, CCA depreciation, tax set-asides, and multi-user profiles were added, it grew into a genuine bookkeeping suite — at which point the old name stopped fitting, and it was renamed to Reconciled to match what it had actually become.

Building

# Run in development
./gradlew run

# Build fat JAR
./gradlew jar

# Package as macOS DMG (must be run on macOS)
./gradlew jpackageMac

# Package as Windows EXE installer (must be run on Windows)
./gradlew jpackageWindows

jpackage can't cross-compile installers for another OS, so each packaging task has to run on that same platform. jpackageWindows defaults to an .exe to avoid requiring the WiX Toolset — switch to --type msi in build.gradle.kts if WiX is available and an .msi is preferred. Requires Java 25+.