File size: 9,332 Bytes
0a367ef
4c83ab6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a367ef
4c83ab6
 
 
 
7da71bc
 
 
 
 
0a367ef
7da71bc
 
 
 
0a367ef
7da71bc
 
0a367ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c55abce
 
 
 
 
 
 
 
4c83ab6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a367ef
4c83ab6
 
 
0a367ef
 
4c83ab6
0a367ef
4c83ab6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a367ef
 
 
 
 
 
 
 
 
 
 
4c83ab6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7da71bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a367ef
7da71bc
0a367ef
7da71bc
 
 
 
 
0a367ef
 
 
7da71bc
0a367ef
 
 
 
 
 
 
 
 
 
7da71bc
 
 
 
0a367ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c83ab6
 
 
 
 
 
 
 
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import json
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from typing import AsyncGenerator

import aiosqlite

_DDL = """
PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;

CREATE TABLE IF NOT EXISTS subscriptions (
    id               TEXT PRIMARY KEY,
    folder_id        TEXT NOT NULL,
    folder_name      TEXT NOT NULL,
    expiry_datetime  TEXT NOT NULL,
    notification_url TEXT NOT NULL,
    client_state     TEXT NOT NULL,
    is_active        INTEGER NOT NULL DEFAULT 1,
    created_at       TEXT NOT NULL DEFAULT (datetime('now')),
    last_renewed_at  TEXT
);

CREATE TABLE IF NOT EXISTS notifications (
    id                INTEGER PRIMARY KEY AUTOINCREMENT,
    received_at       TEXT NOT NULL DEFAULT (datetime('now')),
    subscription_id   TEXT,
    change_type       TEXT,
    resource          TEXT,
    message_id        TEXT,
    email_identifier  TEXT,
    processing_status TEXT NOT NULL DEFAULT 'pending',
    error_message     TEXT,
    storage_path      TEXT
);

CREATE TABLE IF NOT EXISTS routing_results (
    id                  INTEGER PRIMARY KEY AUTOINCREMENT,
    notification_id     INTEGER NOT NULL,
    processed_at        TEXT NOT NULL DEFAULT (datetime('now')),
    email_identifier    TEXT,
    attachment_filename TEXT,
    document_type       TEXT,
    action              TEXT,
    destination_path    TEXT,
    confidence_scores   TEXT,
    error_message       TEXT
);

CREATE TABLE IF NOT EXISTS daily_brief (
    id                  INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at          TEXT NOT NULL DEFAULT (datetime('now')),
    email_identifier    TEXT NOT NULL,
    notification_id     INTEGER NOT NULL,
    sender_email        TEXT,
    subject             TEXT,
    attachment_filename TEXT,
    reason              TEXT NOT NULL,
    confidence_scores   TEXT,
    error_message       TEXT,
    included_in_report  INTEGER NOT NULL DEFAULT 0,
    report_sent_at      TEXT
);

CREATE TABLE IF NOT EXISTS client_domains (
    domain        TEXT PRIMARY KEY,
    client_name   TEXT NOT NULL,
    created_at    TEXT NOT NULL DEFAULT (datetime('now')),
    last_seen_at  TEXT,
    notes         TEXT
);
"""


async def init_db(db_path: Path) -> None:
    db_path.parent.mkdir(parents=True, exist_ok=True)
    async with aiosqlite.connect(db_path) as conn:
        await conn.executescript(_DDL)
        await conn.commit()


@asynccontextmanager
async def get_db(db_path: Path) -> AsyncGenerator[aiosqlite.Connection, None]:
    async with aiosqlite.connect(db_path) as conn:
        conn.row_factory = aiosqlite.Row
        await conn.execute("PRAGMA journal_mode=WAL")
        await conn.execute("PRAGMA foreign_keys=ON")
        yield conn


async def upsert_subscription(
    conn: aiosqlite.Connection,
    sub_id: str,
    folder_id: str,
    folder_name: str,
    expiry_datetime: str,
    notification_url: str,
    client_state: str,
) -> None:
    await conn.execute(
        """
        INSERT INTO subscriptions (id, folder_id, folder_name, expiry_datetime,
            notification_url, client_state, is_active)
        VALUES (?, ?, ?, ?, ?, ?, 1)
        ON CONFLICT(id) DO UPDATE SET
            expiry_datetime  = excluded.expiry_datetime,
            last_renewed_at  = datetime('now'),
            is_active        = 1
        """,
        (sub_id, folder_id, folder_name, expiry_datetime, notification_url, client_state),
    )
    await conn.commit()


async def get_active_subscription(conn: aiosqlite.Connection) -> dict | None:
    cursor = await conn.execute(
        "SELECT * FROM subscriptions WHERE is_active = 1 ORDER BY created_at DESC LIMIT 1"
    )
    row = await cursor.fetchone()
    return dict(row) if row else None


async def mark_subscription_inactive(conn: aiosqlite.Connection, sub_id: str) -> None:
    await conn.execute(
        "UPDATE subscriptions SET is_active = 0 WHERE id = ?", (sub_id,)
    )
    await conn.commit()


async def log_notification(
    conn: aiosqlite.Connection,
    subscription_id: str | None,
    change_type: str | None,
    resource: str | None,
    message_id: str | None,
    email_identifier: str | None = None,
) -> int:
    cursor = await conn.execute(
        """
        INSERT INTO notifications (subscription_id, change_type, resource, message_id, email_identifier)
        VALUES (?, ?, ?, ?, ?)
        """,
        (subscription_id, change_type, resource, message_id, email_identifier or message_id),
    )
    await conn.commit()
    return cursor.lastrowid


async def update_subscription_expiry(
    conn: aiosqlite.Connection, sub_id: str, expiry_datetime: str
) -> None:
    await conn.execute(
        """
        UPDATE subscriptions
        SET expiry_datetime = ?, last_renewed_at = datetime('now')
        WHERE id = ?
        """,
        (expiry_datetime, sub_id),
    )
    await conn.commit()


async def get_notification_by_email_identifier(
    conn: aiosqlite.Connection, email_identifier: str
) -> dict | None:
    cursor = await conn.execute(
        "SELECT * FROM notifications WHERE email_identifier = ? LIMIT 1",
        (email_identifier,),
    )
    row = await cursor.fetchone()
    return dict(row) if row else None


async def update_notification_status(
    conn: aiosqlite.Connection,
    notif_id: int,
    status: str,
    error_message: str | None = None,
    storage_path: str | None = None,
) -> None:
    await conn.execute(
        """
        UPDATE notifications
        SET processing_status = ?, error_message = ?, storage_path = ?
        WHERE id = ?
        """,
        (status, error_message, storage_path, notif_id),
    )
    await conn.commit()


async def get_fetched_notifications(conn: aiosqlite.Connection) -> list[dict]:
    cursor = await conn.execute(
        "SELECT * FROM notifications WHERE processing_status = 'fetched' ORDER BY received_at"
    )
    rows = await cursor.fetchall()
    return [dict(r) for r in rows]


async def set_notification_routing_status(
    conn: aiosqlite.Connection, notif_id: int, status: str
) -> None:
    await conn.execute(
        "UPDATE notifications SET processing_status = ? WHERE id = ?",
        (status, notif_id),
    )
    await conn.commit()


async def log_routing_result(
    conn: aiosqlite.Connection,
    notification_id: int,
    filename: str | None,
    doc_type: str | None,
    action: str,
    email_identifier: str | None = None,
    destination_path: str | None = None,
    confidence_scores: dict | None = None,
    error_message: str | None = None,
) -> None:
    await conn.execute(
        """
        INSERT INTO routing_results
            (notification_id, email_identifier, attachment_filename, document_type, action,
             destination_path, confidence_scores, error_message)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """,
        (
            notification_id,
            email_identifier,
            filename,
            doc_type,
            action,
            destination_path,
            json.dumps(confidence_scores) if confidence_scores else None,
            error_message,
        ),
    )
    await conn.commit()


async def insert_daily_brief(
    conn: aiosqlite.Connection,
    email_identifier: str,
    notification_id: int,
    sender_email: str | None,
    subject: str | None,
    filename: str | None,
    reason: str,
    confidence_scores: dict | None = None,
    error_message: str | None = None,
) -> None:
    await conn.execute(
        """
        INSERT INTO daily_brief
            (email_identifier, notification_id, sender_email, subject,
             attachment_filename, reason, confidence_scores, error_message)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """,
        (
            email_identifier,
            notification_id,
            sender_email,
            subject,
            filename,
            reason,
            json.dumps(confidence_scores) if confidence_scores else None,
            error_message,
        ),
    )
    await conn.commit()


async def get_pending_brief_items(conn: aiosqlite.Connection) -> list[dict]:
    cursor = await conn.execute(
        "SELECT * FROM daily_brief WHERE included_in_report = 0 ORDER BY created_at"
    )
    rows = await cursor.fetchall()
    return [dict(r) for r in rows]


async def mark_brief_items_sent(conn: aiosqlite.Connection, ids: list[int]) -> None:
    await conn.execute(
        f"UPDATE daily_brief SET included_in_report = 1, report_sent_at = datetime('now') "
        f"WHERE id IN ({','.join('?' * len(ids))})",
        ids,
    )
    await conn.commit()


async def run_retention_cleanup(conn: aiosqlite.Connection) -> None:
    await conn.executescript("""
        DELETE FROM notifications   WHERE received_at  < datetime('now', '-12 months');
        DELETE FROM routing_results WHERE processed_at < datetime('now', '-12 months');
        DELETE FROM daily_brief     WHERE created_at   < datetime('now', '-12 months');
        DELETE FROM subscriptions   WHERE is_active = 0 AND created_at < datetime('now', '-12 months');
    """)
    await conn.commit()


if __name__ == "__main__":
    import asyncio

    if "--init" in sys.argv:
        from app.config import settings

        asyncio.run(init_db(settings.database_path))
        print(f"Database initialized at {settings.database_path}")