bank-fraud / migrations /schema.sql
root
init
942b115
Raw
History Blame Contribute Delete
6.5 kB
-- Banking Fraud Detection System -- reproducible MySQL schema.
--
-- This file is generated (not hand-written) from the SQLAlchemy 2.0 typed
-- models in app/db/models.py, compiled against the MySQL dialect via
-- sqlalchemy.schema.CreateTable/CreateIndex. It is the human-reviewable
-- artifact for the DB-02 confirmation checkpoint: this exact DDL (applied
-- via app/db/session.py:create_all(), which calls Base.metadata.create_all())
-- is what gets executed against the database once approved.
--
-- Do not hand-edit this file and expect it to stay in sync with the ORM
-- models -- if the schema changes, regenerate this file from app/db/models.py.
--
-- Updated for the two-role (banking officer / client) auth extension:
-- * Added users (login accounts, bcrypt password hashes, officer/client
-- role) and sessions (server-side session tokens -- chosen over JWT so
-- access can be instantly revoked by deleting the row, rather than
-- waiting out a token's expiry).
-- * test_profiles / test_transactions / test_predictions were removed
-- entirely. test_profiles is replaced by `clients` (banking fields
-- only -- starting_balance, account_type -- plus a 1:1 FK to the users
-- row that logs into it; a client's name lives on users.name, not
-- duplicated). test_transactions/test_predictions are gone: client-
-- submitted transactions now flow into the SAME transactions ->
-- fraud_predictions -> fraud_alerts pipeline as everything else (via a
-- new nullable transactions.client_id), so the officer's alert queue
-- and a client's own alert view are just two filtered queries over one
-- table, not two separate systems.
-- * This was a from-scratch rebuild, not an in-place ALTER/RENAME
-- migration: all 6 prior tables were empty (pre-launch, no real user
-- data existed yet) when this change was made, confirmed with the user
-- before dropping them.
--
-- Design notes for reviewers:
-- * users.role and fraud_alerts.disposition are native MySQL ENUMs.
-- * sessions.token is a unique, indexed opaque random string (not a JWT)
-- -- validated by DB lookup on every authenticated request.
-- * clients.user_id is UNIQUE (1:1 with users) -- a login account either
-- isn't a client, or maps to exactly one client profile.
-- * transactions.client_id is nullable: NULL means officer-submitted
-- (via /predict or /transactions/batch, e.g. backfill/testing), set
-- means client-submitted via /client/transactions. ON DELETE SET NULL
-- preserves transaction history if a client account is ever removed.
-- * transactions.is_fraud is nullable: real-time submitted transactions
-- have no known label at score time; historical PaySim rows loaded for
-- reference/training carry the known label. PaySim's `isFlaggedFraud`
-- is deliberately NOT a column here at all (near-zero recall against
-- isFraud -- see .planning/research/PITFALLS.md Pitfall 2).
-- * fraud_predictions.top_features is a MySQL JSON column holding a
-- structured list of {"feature": <name>, "shap_value": <float>} objects,
-- ordered by descending absolute SHAP value -- queryable, not an opaque
-- blob. model_version records the artifact identifier (e.g. "model_v1").
-- * fraud_alerts.disposition is a native MySQL ENUM('open','reviewed',
-- 'dismissed','rejected'), defaulting to 'open'; reviewed_at is nullable
-- and set when an officer moves an alert out of 'open' -- structured now
-- so a future analyst-labeling/retraining pipeline can consume it
-- without a schema rewrite (DB-04).
-- * transactions.reversed (added alongside 'rejected'): when an officer
-- rejects an alert, the underlying transaction is flagged reversed=1 and
-- excluded from balance calculations -- the reject action's real effect
-- is "give the money back." This was an in-place ALTER (not a rebuild):
-- real user data already existed in transactions/clients at the time.
CREATE TABLE users (
id INTEGER NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
`role` ENUM('officer','client') NOT NULL,
created_at DATETIME NOT NULL DEFAULT now(),
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX ix_users_email ON users (email);
CREATE TABLE clients (
id INTEGER NOT NULL AUTO_INCREMENT,
user_id INTEGER NOT NULL,
created_by_officer_id INTEGER NOT NULL,
starting_balance NUMERIC(18, 2) NOT NULL,
account_type VARCHAR(30) NOT NULL,
created_at DATETIME NOT NULL DEFAULT now(),
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY(created_by_officer_id) REFERENCES users (id) ON DELETE RESTRICT
);
CREATE UNIQUE INDEX ix_clients_user_id ON clients (user_id);
CREATE TABLE sessions (
id INTEGER NOT NULL AUTO_INCREMENT,
token VARCHAR(64) NOT NULL,
user_id INTEGER NOT NULL,
created_at DATETIME NOT NULL DEFAULT now(),
expires_at DATETIME NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX ix_sessions_token ON sessions (token);
CREATE TABLE transactions (
id INTEGER NOT NULL AUTO_INCREMENT,
client_id INTEGER,
step INTEGER NOT NULL,
type VARCHAR(20) NOT NULL,
amount NUMERIC(18, 2) NOT NULL,
name_orig VARCHAR(50) NOT NULL,
oldbalance_org NUMERIC(18, 2) NOT NULL,
newbalance_orig NUMERIC(18, 2) NOT NULL,
name_dest VARCHAR(50) NOT NULL,
oldbalance_dest NUMERIC(18, 2) NOT NULL,
newbalance_dest NUMERIC(18, 2) NOT NULL,
is_fraud BOOL,
reversed BOOL NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT now(),
PRIMARY KEY (id),
FOREIGN KEY(client_id) REFERENCES clients (id) ON DELETE SET NULL
);
CREATE TABLE fraud_predictions (
id INTEGER NOT NULL AUTO_INCREMENT,
transaction_id INTEGER NOT NULL,
probability NUMERIC(6, 5) NOT NULL,
risk_tier VARCHAR(10) NOT NULL,
model_version VARCHAR(50) NOT NULL,
top_features JSON NOT NULL,
created_at DATETIME NOT NULL DEFAULT now(),
PRIMARY KEY (id),
FOREIGN KEY(transaction_id) REFERENCES transactions (id) ON DELETE CASCADE
);
CREATE TABLE fraud_alerts (
id INTEGER NOT NULL AUTO_INCREMENT,
prediction_id INTEGER NOT NULL,
risk_tier VARCHAR(10) NOT NULL,
disposition ENUM('open','reviewed','dismissed','rejected') NOT NULL DEFAULT 'open',
reviewed_at DATETIME,
created_at DATETIME NOT NULL DEFAULT now(),
PRIMARY KEY (id),
FOREIGN KEY(prediction_id) REFERENCES fraud_predictions (id) ON DELETE CASCADE
);