diff --git a/app/src/api/events.ts b/app/src/api/events.ts new file mode 100644 index 000000000..ccb80ad64 --- /dev/null +++ b/app/src/api/events.ts @@ -0,0 +1,5 @@ +import { api } from './client'; +export type Event = { id: number; event_type: string; created_at: string }; +export async function emitEvent(eventType: string): Promise { return api('/events/emit', { method: 'POST', body: { event_type: eventType } }); } +export async function getEvents(limit?: number, type?: string): Promise { const q = new URLSearchParams(); if (limit) q.set('limit', String(limit)); if (type) q.set('type', type); return api('/events/stream?' + q); } +export async function getEventTypes(): Promise<{ types: string[] }> { return api('/events/types'); } diff --git a/packages/backend/app/routes/__init__.py b/packages/backend/app/routes/__init__.py index f13b0f897..1f75d62b6 100644 --- a/packages/backend/app/routes/__init__.py +++ b/packages/backend/app/routes/__init__.py @@ -7,6 +7,7 @@ from .categories import bp as categories_bp from .docs import bp as docs_bp from .dashboard import bp as dashboard_bp +from .events import bp as events_bp def register_routes(app: Flask): @@ -18,3 +19,4 @@ def register_routes(app: Flask): app.register_blueprint(categories_bp, url_prefix="/categories") app.register_blueprint(docs_bp, url_prefix="/docs") app.register_blueprint(dashboard_bp, url_prefix="/dashboard") + app.register_blueprint(events_bp, url_prefix="/events") diff --git a/packages/backend/app/routes/events.py b/packages/backend/app/routes/events.py new file mode 100644 index 000000000..2168b440b --- /dev/null +++ b/packages/backend/app/routes/events.py @@ -0,0 +1,40 @@ +"""Event-driven financial activity system.""" +from datetime import datetime +from flask import Blueprint, jsonify, request +from flask_jwt_extended import jwt_required, get_jwt_identity +from ..extensions import db +from ..models import AuditLog +import logging +bp = Blueprint("events", __name__) + +@bp.post("/emit") +@jwt_required() +def emit_event(): + uid = int(get_jwt_identity()) + data = request.get_json() or {} + event_type = (data.get("event_type") or "").strip() + if not event_type: + return jsonify(error="event_type required"), 400 + entry = AuditLog(user_id=uid, action=f"event:{event_type}") + db.session.add(entry) + db.session.commit() + return jsonify(id=entry.id, event_type=event_type, created_at=entry.created_at.isoformat()), 201 + +@bp.get("/stream") +@jwt_required() +def get_events(): + uid = int(get_jwt_identity()) + limit = min(int(request.args.get("limit", 50)), 200) + event_type = request.args.get("type") + q = db.session.query(AuditLog).filter_by(user_id=uid) + if event_type: + q = q.filter(AuditLog.action == f"event:{event_type}") + events = q.order_by(AuditLog.created_at.desc()).limit(limit).all() + return jsonify([{"id": e.id, "event_type": e.action.replace("event:", ""), "created_at": e.created_at.isoformat()} for e in events]) + +@bp.get("/types") +@jwt_required() +def event_types(): + uid = int(get_jwt_identity()) + types = db.session.query(AuditLog.action).filter_by(user_id=uid).filter(AuditLog.action.like("event:%")).distinct().all() + return jsonify(types=[t[0].replace("event:", "") for t in types]) diff --git a/packages/backend/tests/test_events.py b/packages/backend/tests/test_events.py new file mode 100644 index 000000000..f0f1f2c88 --- /dev/null +++ b/packages/backend/tests/test_events.py @@ -0,0 +1,19 @@ +def test_auth(client): assert client.get("/events/stream").status_code in (401, 422) +def test_emit(client, auth_header): + r = client.post("/events/emit", json={"event_type": "expense.created"}, headers=auth_header) + assert r.status_code == 201 + assert r.get_json()["event_type"] == "expense.created" +def test_stream(client, auth_header): + client.post("/events/emit", json={"event_type": "test"}, headers=auth_header) + r = client.get("/events/stream", headers=auth_header) + assert r.status_code == 200 + assert len(r.get_json()) >= 1 +def test_types(client, auth_header): + client.post("/events/emit", json={"event_type": "foo"}, headers=auth_header) + r = client.get("/events/types", headers=auth_header) + assert "foo" in r.get_json()["types"] +def test_filter(client, auth_header): + client.post("/events/emit", json={"event_type": "alpha"}, headers=auth_header) + client.post("/events/emit", json={"event_type": "beta"}, headers=auth_header) + r = client.get("/events/stream?type=alpha", headers=auth_header) + assert all(e["event_type"] == "alpha" for e in r.get_json())