-- 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": , "shap_value": } 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 );