Skip to main content
YK logo
← Back to projects

Wells Dealer Perfs — Building a Finance Preference Portal from the Ground Up

Ascent dealer dashboard

Overview

Wells Dealer Perfs is a web portal that allows car dealers to configure their finance preferences across five credit ranges. Dealers log in, browse available programs — each with different interest rates, tenors, dealer fees, and payment factors — and select their preferred option per range. Those selections feed into a downstream workflow that governs what finance products get surfaced to customers. It's a relatively contained UI, but the business logic underneath it matters quite a lot: a dealer's choices here have a direct effect on the deals their customers are offered.

The Problem

I came onto this project without a clear brief. There was no detailed spec, and the product direction shifted regularly throughout delivery. The core challenge was less about any single technical problem and more about building something coherent when the requirements kept moving — understanding what the application actually needed to do, then engineering it in a way that could absorb change without falling apart.

On top of that, I was working within a design system where the underlying code was genuinely dated. The components existed, but they hadn't been modernised to work with current tooling or patterns, which meant I couldn't just consume them and move on — they needed reworking to be usable.

My Approach

I started by reverse-engineering what I could from the existing design system and the API contracts, building a mental model of what the application needed to do before writing anything significant. That meant spending real time with the data shapes — particularly the Preference type, which carries interest rates, tenors, dealer fees, payment factors, and credit range metadata — and working out how selection state needed to flow through the UI before committing to an architecture.

For the preference selection itself, I used TanStack Table v8 to render each credit range's program data. TanStack is headless, which suited the situation well — I had full control over the rendered markup while the library handled the more mechanical parts of working with tabular data. Each range gets its own DataTable instance, wrapped in a collapsible Accordion, with radio button selection per row. Selections are held in state and persisted to localStorage until the user reviews and submits.

One of the more interesting pieces to get right was the preference locking. Once preferences are submitted, they become read-only until a specific unlock time. The logic sits across a handful of utility functions (getPreferenceUnlockTime, setPreferenceUnlockTime) and the DataTable component's readonly prop. It's not complex in isolation, but getting the timing checks right — particularly around the isBefore comparison from date-fns — required care.

The i18n setup was something I put deliberate thought into. Rather than managing translation strings ad-hoc, I set up a FormatJS pipeline where all strings live in .messages.ts files co-located with their components. A preextract-messages script runs FormatJS extract across the entire src tree, compiles the output to public/i18n/en-GB.json, and cleans up the temp file. The start script runs this automatically so the locale file is always in sync before the dev server spins up. When requirements changed and copy needed updating — which happened often — the process was straightforward: update the message file, re-run, done. No hunting through a single monolithic strings file.

I also modernised the design system components where needed, pulling them into a shape that worked with React 18 and the rest of the stack, rather than working around them.

What I Built

Pretty much everything. The full auth flow — login (with account locking and a countdown timer for failed attempts), registration, password reset, set password, and confirm password screens. The preferences page, which loads program data asynchronously via a React Router v6 loader, renders five accordioned data tables, tracks selection state locally, and guards against submission when preferences are locked. The preference review and confirmation screens. A print-ready rate sheet view that calls window.print() on mount. The i18n pipeline end to end. The test suite, using Vitest and Testing Library for unit tests, and Cypress for E2E.

Challenges & Tradeoffs

The biggest ongoing challenge was working without clear direction. That forced me to make a lot of judgement calls — about which requirements to prioritise, where to invest in structure, and when to keep things simple rather than over-engineer for a future that hadn't been defined. In retrospect, I think I got that balance mostly right, but there were definitely moments where I built something only to revisit it when the brief shifted.

The dated design system was a real friction point early on. I had to decide whether to work around it or fix it, and fixing it turned out to be the only sensible path — working around it would have left technical debt that compounded with every new screen. That call cost time upfront but paid off as the project grew.

Outcome

The application shipped and went live, with tens of thousands of dealers using it. More tellingly, it became the foundation for something bigger: the codebase eventually evolved into Ascent, a monorepo housing four applications. What started as a single preference portal — built by one engineer, without a spec — became the architectural starting point for a much larger product. That's probably the most useful measure of whether the early decisions were sound.