-- 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
);
