Overview
Aqua Finance is the consumer-facing application journey for marine and outdoor-leisure dealer financing — the multi-screen flow a customer works through to apply for financing at the point of sale, from the initial quote through document review to a signed contract. It's a sibling project to Ascent, built on a similar stack but for a different lender and dealer network, with its own set of issuer-specific requirements.
The problem
Consumer finance applications live or die on the small stuff: a phone number field that rejects a valid US number, a footer that shows the wrong lender's disclosures, a date picker that lets someone enter an invalid date of birth. None of these are hard problems individually, but a lending flow has dozens of them, and each one is a point where a real customer can get stuck applying for money they're trying to spend at a dealer, in person, while someone waits.
What I built
Issuer-controlled footer
The footer's content — disclosures, lender branding, links — needed to switch based on which issuing bank was backing a given application, rather than being fixed per deployment. I made the footer read from issuer configuration so the same build could serve multiple issuers correctly, which matters for a white-label-adjacent lending product where the legal disclosures genuinely differ by issuer.
US phone number validation
Added proper US telephone-number validation and a phone field on the document review screen, using pattern-based validation rather than a loose regex that would let through numbers that look plausible but aren't dialable.
Input hardening
Restricting date inputs to sensible ranges, issuer-specific disclaimers, and fixing a bug where the frontend's index page would 404 instead of redirecting into the flow — a small thing, but the kind of small thing that looks like the whole app is broken if a customer hits it first.
Removing moment entirely
Migrated every date-handling call off moment in favour of date-fns. moment is a large, no-longer-actively-developed dependency, and carrying it just for date formatting was pure bundle weight and future maintenance risk. This is unglamorous dependency hygiene, but on a lending app where every extra kilobyte is time-to-interactive on someone's phone in a dealership, it's worth doing properly rather than leaving for later.
Engineering decisions
Configuration over conditionals for issuer differences
The footer and disclosure work could have been done as if (issuer === 'x') branches scattered through components. I pushed the issuer-specific content into configuration instead, so adding a new issuer's disclosures is a data change rather than a new code path to test. It's the same lesson I kept relearning across the Luna Protocol projects — the parts of a multi-issuer product that feel like plumbing are the parts most worth getting right early.
A full dependency removal, not a partial migration
Rather than leaving moment in place for legacy call sites and using date-fns only for new code — the usual compromise under time pressure — I went through and removed every moment usage. Partial migrations are how a codebase ends up depending on two date libraries indefinitely; finishing the job was worth the extra time.
Stack
| Layer | Choice |
|---|---|
| Framework | React · Create React App |
| State | Redux Toolkit |
| Forms | Formik + Yup |
| Styling | styled-components · Sass · Tailwind CSS |
| Testing | Cypress (Cucumber preprocessor) · Jest · Testing Library · MSW |
| Email test | gmail-tester for email-based E2E flows |
Reflections
This project is a good reminder that not every meaningful contribution is a new feature. The moment removal, the phone validation, the issuer-controlled footer — none of these show up in a demo, but they're the difference between a lending flow that works for every customer in every scenario and one that mostly works. On a product where the "unhappy path" is someone standing in a dealership unable to finish applying, "mostly works" isn't good enough.