File size: 6,504 Bytes
942b115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
-- 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
);