Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 21 additions & 39 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,19 @@
# The jobs run the scripts rather than repeating what the scripts do. A workflow that spelled the
# steps out again would be a second copy of the setup instructions, free to drift from the ones a
# reader follows, and drift is exactly what this is here to catch.
name: build
name: CI

# main and every pull request. A feature branch nobody has opened a pull request for is checked by
# hand through workflow_dispatch; running on every push as well would mean two identical runs for
# every branch that later becomes a pull request.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

# Nothing here writes anything back. Stated rather than left to the repository default, which an
# organisation setting can widen without anyone touching this file.
permissions:
contents: read

# A newer commit on the same branch makes the run in progress pointless.
concurrency:
group: build-${{ github.ref }}
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand All @@ -32,52 +26,43 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-java@v4
- uses: actions/setup-java@v6
with:
distribution: temurin
# 21 and not 17, because 21 is the JDK this project is developed and measured on.
# Nothing newer than 17 can slip into the sources anyway: maven-compiler-plugin pins
# release to 17, which bounds the API surface rather than only the bytecode version.
java-version: '21'
# 17, which is what maven-compiler-plugin pins release to. Building on the version the
# project is held to is the configuration its tests are green on.
java-version: '17'
cache: maven

# sh ./mvnw, not ./mvnw. The repository is worked on from Windows with core.filemode false,
# so a file added there carries mode 100644 and the wrapper is not executable in a clone
# made here. Naming the interpreter costs nothing and does not depend on that mode. The
# ./ is not decoration: the wrapper reads its properties from ${0%/*}/.mvn/wrapper, and a
# bare mvnw has no slash to strip, so the path becomes mvnw/.mvn/wrapper and the read fails.
# made here. The ./ is not decoration: the wrapper reads its properties from
# ${0%/*}/.mvn/wrapper, and a bare mvnw has no slash to strip.
- name: Maven wrapper
run: sh ./mvnw -B -v

# The stack the README describes, started by the script the README names. Deliberately not
# a service container: db/init is mounted by docker-compose.yml and runs only when compose
# starts PostgreSQL, and it is what creates minibank_test and applies the schema to both
# databases. A service container would leave the second database missing and the SQL tests
# would report as skipped instead of failing.
# The stack the README describes, started by the script the README names. Deliberately not a
# service container: db/init is mounted by docker-compose.yml and runs only when compose
# starts PostgreSQL, and it is what creates minibank_test and applies the schema to it.
- name: Start PostgreSQL
run: sh scripts/up.sh

# Every migration, against both databases, on a volume built moments ago from
# db/init/schema.sql. All of them are written to be safe to run twice, and this is what
# keeps that claim true: one that stopped being re-runnable fails here, rather than on the
# machine of whoever next resets a database.
# db/init/schema.sql. All of them are written to be safe to run twice, and this is what keeps
# that claim true.
- name: Apply the migrations
run: sh scripts/migrate.sh

- name: Build the backend
run: sh ./mvnw -B -DskipTests package

# The test database is named in full, and that address is not optional. Without the property
# the SQL tests skip and the run goes green over untested code; with an address that names
# some other database they TRUNCATE it, because that is what they do to the database they
# are given.
# the SQL tests skip and the run goes green over untested code.
- name: Test the backend
run: sh ./mvnw -B test "-Dminibank.test.sql.url=jdbc:postgresql://localhost:55432/minibank_test"

# Surefire redirects test output to files, so a failure prints a summary and keeps the
# detail on disk. On the runner that disk is discarded with the job unless it is collected.
- name: Keep the reports of a failed run
if: failure()
uses: actions/upload-artifact@v4
Expand All @@ -91,14 +76,11 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
# The floor both package.json files declare, which is in turn the floor the installed
# Vite declares. Pinned to the minor rather than to '22' so that this file says which
# version the front ends were checked on.
node-version: '22.12'
node-version: '24'
cache: npm
cache-dependency-path: |
minibank-web/package-lock.json
Expand All @@ -109,8 +91,8 @@ jobs:
run: npm ci

# Installed even though nothing is run from this directory below: the test step reaches the
# workstation's sources through its own vite config, and that config imports the React
# plugin from here.
# workstation's sources through its own vite config, and that config imports the React plugin
# from here.
- name: Install the workstation
working-directory: minibank-fraud-web
run: npm ci
Expand All @@ -136,4 +118,4 @@ jobs:

- name: Build the workstation
working-directory: minibank-fraud-web
run: npm run build
run: npm run build
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ node_modules/

### Runtime artifacts ###
*.log
*.stackdump
storage/
data/

Expand Down
126 changes: 113 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,104 @@
# MiniBank

A small retail banking application written to show persistence patterns implemented by hand, with
no ORM anywhere in it: a Unit of Work that defers every write to commit, an Identity Map that makes
one transaction see one instance of a row, and Lazy Load behind the navigation properties. The
domain layer has no framework in it at all, and the same domain runs against two different stores.
[![CI](https://github.com/d3m1d0s/VIS_mini-bank/actions/workflows/ci.yml/badge.svg)](https://github.com/d3m1d0s/VIS_mini-bank/actions/workflows/ci.yml)

What it does: a customer signs in, lists their accounts, sends a payment, confirms it with a one
time password, or cancels it. A payment that looks risky is held for review, and a fraud analyst
approves it, records confirmed fraud, or leaves notes.
MiniBank is a full-stack retail banking application built to demonstrate production-style backend
design without an ORM. It combines a Java 17 and Spring Boot REST API, PostgreSQL/JDBC persistence,
and React/TypeScript clients with hand-written Unit of Work, Identity Map and Lazy Loading patterns.

Customers can review accounts and payment history, create and confirm payments with a one-time
password, and cancel pending transfers. Risky payments enter a separate fraud-review workflow where
an analyst can investigate the alert, leave notes and approve or decline the transfer.

**Stack:** Java 17, Spring Boot, REST, PostgreSQL, JDBC, React, TypeScript, Vite, Maven, Docker,
JUnit 5, Mockito, Vitest and ESLint.

![Customer creating a bank transfer](docs/screenshots/customer-new-payment.png)

*Customer payment creation with beneficiary selection, real-time fee calculation and daily-limit tracking.*

## Highlights

- End-to-end payment flow with authentication, OTP authorization, cancellation and history.
- Rule-based risk checks and a dedicated fraud analyst workflow.
- Framework-independent domain model with repository and Unit of Work abstractions.
- JSON and PostgreSQL persistence adapters behind the same application services.
- Optimistic locking for accounts, transfers and fraud alerts to prevent lost updates.
- Parameterized JDBC queries, transactional writes and automated backend/frontend tests.

## Product walkthrough

### OTP payment authorization

![Customer authorizing a pending payment using a one-time password](docs/screenshots/customer-otp-authorization.png)

*Pending transfers require OTP confirmation and expose retry limits, expiration time and fraud-review status.*

### Fraud review

![Fraud analyst reviewing a risk alert](docs/screenshots/fraud-alert-review.png)

*Rule-based alerts enter a dedicated analyst queue with assignment, customer history, case notes and approve/decline decisions.*

<details>
<summary><strong>View the complete fraud investigation workflow</strong></summary>

<br>

<p align="center">
<img
src="docs/screenshots/fraud-more-details.png"
alt="Full fraud analyst workflow with alert details, customer history, case notes and decision form"
width="760"
>
</p>

<p align="center">
<em>Complete analyst workspace with alert facts, transaction history, case notes and the final decision form.</em>
</p>

</details>

> All accounts, transactions and credentials shown above are synthetic demo data.

## Architecture

```mermaid
flowchart TD
UI["React + TypeScript clients"] --> API["Spring Boot REST API"]
API --> APP["Application services"]
APP --> DOMAIN["Framework-free domain model"]
APP --> PORTS["Repository + Unit of Work interfaces"]
PORTS --> JSON["JSON adapter"]
PORTS --> JDBC["JDBC adapter"]
JDBC --> DB[(PostgreSQL)]
```

The domain and application layers do not depend on Spring or a persistence technology. Infrastructure
adapters implement the repository and Unit of Work interfaces for either a local JSON store or
PostgreSQL. See [`src/Project_structure.md`](src/Project_structure.md) for the complete package map.

## Quick start

The default demo uses the JSON store, so PostgreSQL and Docker are not required. Run these three
commands, using a second terminal for the web client:

```bash
git clone https://github.com/d3m1d0s/VIS_mini-bank.git && cd VIS_mini-bank
mvn -B spring-boot:run
```

```bash
cd VIS_mini-bank/minibank-web && npm ci && npm run dev
```

Open `http://localhost:5173` and sign in as `alice / alice123` for the customer application or
`fraud / fraud123` for the analyst desk. The demo one-time password is `0000`.

For PostgreSQL mode, both frontends, console applications and detailed configuration, continue with
[Running it](#running-it).

## Design notes
Every push to `main` and every pull request runs `.github/workflows/ci.yml`: the backend is built
and tested against a real PostgreSQL started by docker compose, the migrations are applied to both
databases, and both front ends are linted, tested and built.
Expand All @@ -30,14 +120,11 @@ databases, and both front ends are linted, tested and built.
is ever concatenated into SQL text. The only plain `Statement`s are the constant-text `nextval`
queries that allocate ids.

`src/Project_structure.md` maps the tree.

## Requirements

- Java 17 or newer. The build holds the code to the Java 17 API, so a current JDK works too.
- Docker, for PostgreSQL mode and for the SQL tests
- Node 20.19 or newer, or 22.12 or newer. That is the floor both `package.json` files declare, and
it is in turn the floor the installed Vite declares; the workflow runs 22.12.
- Node 20.19+ or 22.12+; Node 24 is recommended and used in CI

Maven is not on that list. The repository carries the Maven wrapper, `mvnw`, `mvnw.cmd` and
`.mvn/`, which downloads Maven 3.9.6 on first use, so nothing has to be on `PATH`.
Expand Down Expand Up @@ -231,7 +318,7 @@ tables with `.\scripts\demo-reset.ps1`, then start the API again with the demo a
them. JSON mode is untouched: its users live only in memory, so nothing hashed under the old
parameters survives a restart.

## Tests
## Tests and quality checks

```
.\mvnw.cmd -B test
Expand All @@ -255,8 +342,21 @@ There is one JavaScript test runner in the tree. vitest is installed only in `mi
reaches the workstation and `frontend-shared` through the `projects` list in
`minibank-web/vite.config.ts`:

```bash
cd minibank-web
npm ci
npm run lint
npm test
npm run build
```
cd minibank-web; npm test

The standalone fraud application has its own lint and production-build gates:

```bash
cd minibank-fraud-web
npm ci
npm run lint
npm run build
```

`minibank-fraud-web` has no `test` script and is not meant to get one, so `npm test` inside that
Expand Down
30 changes: 0 additions & 30 deletions bash.exe.stackdump

This file was deleted.

Binary file added docs/screenshots/customer-new-payment.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/customer-otp-authorization.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/fraud-alert-review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/fraud-more-details.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions minibank-fraud-web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion minibank-fraud-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"lint": "eslint . --max-warnings=0",
"preview": "vite preview"
},
"dependencies": {
Expand Down
8 changes: 6 additions & 2 deletions minibank-fraud-web/src/FraudDesk.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { useEffect, useEffectEvent, useRef, useState, type ReactNode } from 'react';
import {
buildDecisionRequest,
decisionAllowed,
Expand Down Expand Up @@ -663,10 +663,14 @@ export default function FraudDesk(props: {
? historyErr.failure
: null;

const reloadListForFilterChange = useEffectEvent(() => {
void reloadList();
});

// amountReason belongs in here beside filters. Typing something that cannot be read into an
// already empty field leaves the filters untouched, so on filters alone nothing would re-run
// and the analyst would be told nothing at all.
useEffect(() => { void reloadList(); }, [filters, amountReason]);
useEffect(() => { reloadListForFilterChange(); }, [filters, amountReason]);

/*
* The address, when it says something other than what is on screen.
Expand Down
6 changes: 3 additions & 3 deletions minibank-web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading