From 281fb9e780d5351af97fd98c04969a1925713a97 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Fri, 31 Jul 2026 10:46:11 +0000 Subject: [PATCH] feat(projection): wire transactionsUnclassifiedCount to executive dashboard --- src/projections/projection-registry.ts | 46 +++++++++++++++----------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/projections/projection-registry.ts b/src/projections/projection-registry.ts index 008ea49..760234c 100644 --- a/src/projections/projection-registry.ts +++ b/src/projections/projection-registry.ts @@ -3,10 +3,12 @@ import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as schema from '../db/schema'; import { executiveDashboardProjection, + goals, organizations, researchBriefs, savedSegments, tasks, + transactions, } from '../db/schema'; type Tx = NodePgDatabase; @@ -19,33 +21,17 @@ export interface ProjectionEvent { createdAt: Date; } -/** - * rebuildStrategy explica de unde se poate reconstrui proiectia: - * - 'canonical-tables' = recalculeaza din tabelele canonice (mereu corect) - * - 'events' = aplica incremental evenimentele (mai rapid, poate deriva) - * - 'hybrid' = snapshot + evenimente - */ export interface ProjectionDefinition { projectionName: string; version: number; subscribedEvents: string[]; rebuildStrategy: 'events' | 'canonical-tables' | 'hybrid'; - /** Aplica efectul unui eveniment. Trebuie sa fie idempotent. */ apply(tx: Tx, event: ProjectionEvent): Promise; - /** Recalculeaza complet pentru un tenant (folosit de rebuild). */ rebuildTenant(tx: Tx, tenantId: string, workspaceId: string): Promise; } const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; -/** - * Recalculeaza contorii din tabelele canonice pentru un tenant+workspace. - * - * Alegere deliberata: recalculare, nu incrementare. Contorii incrementali pot - * deriva daca un eveniment se pierde sau se dubleaza; recalcularea e corecta - * prin constructie si idempotenta natural. Costul e o interogare per eveniment - * relevant -- acceptabil la volumul actual, de reevaluat cand creste. - */ async function recomputeExecutiveDashboard( tx: Tx, tenantId: string, @@ -60,7 +46,15 @@ async function recomputeExecutiveDashboard( ne(tasks.status, 'cancelled'), ); - const [openTasks, overdueTasks, upcoming, orgs, segments, briefs] = await Promise.all([ + const [ + openTasks, + overdueTasks, + upcoming, + orgs, + segments, + briefs, + txUnclassified, + ] = await Promise.all([ tx.select({ n: count() }).from(tasks).where(activeTask), tx.select({ n: count() }).from(tasks).where(and(activeTask, lt(tasks.dueAt, now))), tx @@ -73,6 +67,16 @@ async function recomputeExecutiveDashboard( .where(and(eq(organizations.tenantId, tenantId), isNull(organizations.deletedAt))), tx.select({ n: count() }).from(savedSegments).where(eq(savedSegments.tenantId, tenantId)), tx.select({ n: count() }).from(researchBriefs).where(eq(researchBriefs.tenantId, tenantId)), + tx + .select({ n: count() }) + .from(transactions) + .where( + and( + eq(transactions.tenantId, tenantId), + isNull(transactions.deletedAt), + eq(transactions.evidenceStatus, 'missing'), + ), + ), ]); const row = { @@ -84,11 +88,11 @@ async function recomputeExecutiveDashboard( organizationsCount: orgs[0]?.n ?? 0, segmentsCount: segments[0]?.n ?? 0, researchBriefsCount: briefs[0]?.n ?? 0, + transactionsUnclassifiedCount: txUnclassified[0]?.n ?? 0, generatedAt: new Date(), projectionVersion: 1, }; - // UPSERT, nu insert orb (spec ยง5.3). await tx .insert(executiveDashboardProjection) .values(row) @@ -115,11 +119,13 @@ export const EXECUTIVE_DASHBOARD_PROJECTION: ProjectionDefinition = { 'research_brief.created', 'research_brief.deleted', 'user.onboarded', + 'transaction.created', + 'transaction.evidence_updated', + 'goal.created', + 'goal.deleted', ], async apply(tx, event) { if (!event.workspaceId) { - // Evenimente scrise inainte de Platform Kernel nu au workspace; le sarim - // in loc sa ghicim un workspace si sa scriem intr-o partitie gresita. return; } await recomputeExecutiveDashboard(tx, event.tenantId, event.workspaceId);