import { pgTable, pgEnum, uuid, text, timestamp, jsonb, integer, numeric, uniqueIndex, index, } from 'drizzle-orm/pg-core'; // --- Business Engine (blueprint sectiunea 5, 12): companii, tranzactii, documente, taskuri --- // Organizations e sursa de adevar in CEO Postgres pentru companii private (blueprint sectiunea 9); // ERPNext devine sursa de adevar doar pentru "Accounting official", dupa activare -- vezi // Blueprint v4.0 sectiunea 9 si Anexa I decizia #3. export const organizations = pgTable('organizations', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), name: text('name').notNull(), legalName: text('legal_name'), country: text('country'), registryId: text('registry_id'), domain: text('domain'), externalIds: jsonb('external_ids').notNull().default({}), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), deletedAt: timestamp('deleted_at'), version: integer('version').notNull().default(1), }); export const transactionEvidenceStatus = pgEnum('transaction_evidence_status', [ 'missing', 'partial', 'complete', 'not_required', ]); // amounts in minor units (cents), niciodata float, per blueprint 12.1 export const transactions = pgTable('transactions', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), organizationId: uuid('organization_id').notNull(), type: text('type').notNull(), amountMinorUnits: numeric('amount_minor_units', { precision: 18, scale: 0 }).notNull(), currency: text('currency').notNull(), transactionDate: timestamp('transaction_date').notNull(), evidenceStatus: transactionEvidenceStatus('evidence_status').notNull().default('missing'), source: text('source'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), deletedAt: timestamp('deleted_at'), version: integer('version').notNull().default(1), }); export const documentClassification = pgEnum('document_classification', ['c0', 'c1', 'c2', 'c3', 'c4']); // CEO OS pastreaza doar metadata/ownership/permission decisions (blueprint sectiunea 13); // binarul si OCR-ul raman in Paperless-ngx, legate prin paperlessId export const documents = pgTable('documents', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), organizationId: uuid('organization_id'), ownerUserId: uuid('owner_user_id').notNull(), classification: documentClassification('classification').notNull().default('c2'), paperlessId: text('paperless_id'), filename: text('filename').notNull(), checksum: text('checksum'), expiresAt: timestamp('expires_at'), retentionPolicy: text('retention_policy'), ocrStatus: text('ocr_status'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), deletedAt: timestamp('deleted_at'), version: integer('version').notNull().default(1), }); export const taskStatus = pgEnum('task_status', ['open', 'in_progress', 'blocked', 'done', 'cancelled']); export const tasks = pgTable('tasks', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), ownerUserId: uuid('owner_user_id').notNull(), title: text('title').notNull(), priority: integer('priority').notNull().default(3), dueAt: timestamp('due_at'), sourceEntityType: text('source_entity_type'), sourceEntityId: uuid('source_entity_id'), status: taskStatus('status').notNull().default('open'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), deletedAt: timestamp('deleted_at'), version: integer('version').notNull().default(1), }); // --- Identity Engine (blueprint sectiunea 11, 8.2, 8.3) --- export const tenants = pgTable('tenants', { id: uuid('id').defaultRandom().primaryKey(), name: text('name').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), }); // Tenant != Workspace. Tenantul e clientul izolat; workspace-ul e contextul // efectiv de lucru DIN tenant (personal / familie / o anumita firma / comunitate). // Un user poate avea roluri diferite in workspace-uri diferite ale aceluiasi tenant. export const workspaceType = pgEnum('workspace_type', [ 'personal', 'family', 'business', 'community', 'project', ]); export const workspaces = pgTable( 'workspaces', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), name: text('name').notNull(), type: workspaceType('type').notNull().default('business'), /** Workspace-ul creat automat la onboarding; nu poate fi sters. */ isDefault: integer('is_default').notNull().default(0), createdAt: timestamp('created_at').defaultNow().notNull(), createdBy: uuid('created_by'), deletedAt: timestamp('deleted_at'), }, (table) => [index('workspaces_tenant_idx').on(table.tenantId)], ); export const membershipRole = pgEnum('membership_role', ['owner', 'admin', 'member']); // user_id refera auth.users din Supabase Auth (8.1: "user identity si sessions" = Supabase Auth, // nu duplicam identitatea aici, doar apartenenta la tenant + rol) export const memberships = pgTable( 'memberships', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), userId: uuid('user_id').notNull(), role: membershipRole('role').notNull().default('member'), // NULL = membership la nivel de tenant (valabil in toate workspace-urile). // Setat = rol doar in acel workspace, cum cere specul: acelasi user poate fi // owner in Personal si accountant intr-un workspace de firma. workspaceId: uuid('workspace_id'), // Acces delegat cu expirare (ex. contabil pana la 31.12). NULL = fara limita. validFrom: timestamp('valid_from'), validUntil: timestamp('valid_until'), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => [ // un user are cel mult un membership per tenant; lookup-ul din SessionGuard // e pe exact aceasta pereche la fiecare request uniqueIndex('memberships_tenant_user_uq').on(table.tenantId, table.userId), index('memberships_user_idx').on(table.userId), index('memberships_workspace_idx').on(table.workspaceId), ], ); export const consentRecords = pgTable('consent_records', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), userId: uuid('user_id').notNull(), purpose: text('purpose').notNull(), grantedAt: timestamp('granted_at').defaultNow().notNull(), revokedAt: timestamp('revoked_at'), }); // --- Life Engine (blueprint sectiunea 5): obiective personale, rutine, KPI --- export const goalStatus = pgEnum('goal_status', ['not_started', 'on_track', 'at_risk', 'achieved', 'abandoned']); export const goals = pgTable('goals', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), ownerUserId: uuid('owner_user_id').notNull(), horizon: text('horizon').notNull(), metric: text('metric').notNull(), target: text('target').notNull(), milestones: jsonb('milestones').notNull().default([]), status: goalStatus('status').notNull().default('not_started'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), deletedAt: timestamp('deleted_at'), version: integer('version').notNull().default(1), }); // --- Intelligence Engine (blueprint sectiunea 5): decizii, oportunitati, cereri AI --- export const decisions = pgTable('decisions', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), ownerUserId: uuid('owner_user_id').notNull(), context: text('context').notNull(), options: jsonb('options').notNull().default([]), assumptions: jsonb('assumptions').notNull().default([]), evidence: jsonb('evidence').notNull().default([]), selectedOption: text('selected_option'), outcomeReview: text('outcome_review'), reviewDueAt: timestamp('review_due_at'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); export const opportunities = pgTable('opportunities', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), source: text('source').notNull(), entityType: text('entity_type'), entityId: text('entity_id'), valueRangeMin: numeric('value_range_min', { precision: 18, scale: 0 }), valueRangeMax: numeric('value_range_max', { precision: 18, scale: 0 }), probability: numeric('probability', { precision: 5, scale: 4 }), nextAction: text('next_action'), expiresAt: timestamp('expires_at'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); // context_manifest_hash leaga cererea de Context Manifest-ul trimis catre AI Gateway // (blueprint sectiunea 14.1) -- payload-ul complet nu se stocheaza aici, doar hash-ul, // pentru audit fara a pastra continut sensibil in Postgres // costUsd e numeric(12,6), NU integer "minor units": costurile AI reale sunt // fractiuni de cent (ex. $0.00007 per research brief draft) -- rotunjirea la // cent ar sterge complet semnalul de cost. contextManifest se pastreaza integral // (nu doar hash-ul) pentru audit -- blueprint 9 "AI audit: prompt template // version, context manifest, cost si rezultat". export const aiRequests = pgTable('ai_requests', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), requestedByUserId: uuid('requested_by_user_id').notNull(), purpose: text('purpose').notNull(), actionClass: text('action_class').notNull(), contextManifestHash: text('context_manifest_hash').notNull(), contextManifest: jsonb('context_manifest').notNull(), model: text('model').notNull(), templateVersion: text('template_version'), costUsd: numeric('cost_usd', { precision: 12, scale: 6 }), resultStatus: text('result_status').notNull().default('pending'), createdAt: timestamp('created_at').defaultNow().notNull(), completedAt: timestamp('completed_at'), }); // --- Trust Engine (blueprint sectiunea 5): dovezi, observatii, reputation factors --- // subiectul poate fi orice entitate (user, organization, device) -- identificat prin // subjectType+subjectId in loc de FK rigid, ca sa ramana generic pe tot ce alimenteaza // scoruri de incredere/reputatie export const observations = pgTable('observations', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), subjectType: text('subject_type').notNull(), subjectId: text('subject_id').notNull(), metric: text('metric').notNull(), value: text('value').notNull(), unit: text('unit'), source: text('source').notNull(), observedAt: timestamp('observed_at').defaultNow().notNull(), confidence: numeric('confidence', { precision: 5, scale: 4 }), }); // --- Event Fabric / Outbox (blueprint sectiunea 9.1, principiul 2.1 "Events before intelligence") --- // Event envelope complet (spec sectiunea 11). Evenimentele se numesc ca FAPTE // petrecute (document.uploaded), niciodata ca instructiuni (process.document.now). export const outboxEvents = pgTable( 'outbox_events', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), eventType: text('event_type').notNull(), eventVersion: integer('event_version').notNull().default(1), /** Cand s-a petrecut faptul (poate diferi de cand a fost scris randul). */ occurredAt: timestamp('occurred_at').defaultNow().notNull(), actorId: uuid('actor_id'), aggregateType: text('aggregate_type'), /** Pastrat ca subject_id pentru compatibilitate cu dispatcher-ul existent. */ subjectId: uuid('subject_id'), payload: jsonb('payload').notNull(), /** Leaga toate operatiunile din acelasi workflow. */ correlationId: uuid('correlation_id'), /** Ce comanda sau eveniment a produs acest eveniment. */ causationId: uuid('causation_id'), /** C0-C4; impiedica distribuirea catre servicii neautorizate. */ classification: text('classification').notNull().default('c2'), provenance: jsonb('provenance').notNull().default({}), createdAt: timestamp('created_at').defaultNow().notNull(), processedAt: timestamp('processed_at'), }, (table) => [index('outbox_unprocessed_idx').on(table.processedAt, table.createdAt)], ); // --- Audit (blueprint sectiunea 31.4) --- // --- Projection Kernel (spec Read Models sectiunile 2-6) ------------------- // Read models NU sunt sursa de adevar: pot fi sterse si reconstruite oricand // din tabelele canonice. Userul nu le modifica direct. /** * Garantia de idempotency: acelasi eveniment procesat de doua ori de aceeasi * proiectie nu produce efect dublu. Cheia unica e (proiectie, versiune, event). */ export const projectionProcessedEvents = pgTable( 'projection_processed_events', { id: uuid('id').defaultRandom().primaryKey(), projectionName: text('projection_name').notNull(), projectionVersion: integer('projection_version').notNull(), eventId: uuid('event_id').notNull(), processedAt: timestamp('processed_at').defaultNow().notNull(), }, (table) => [ uniqueIndex('projection_processed_uq').on( table.projectionName, table.projectionVersion, table.eventId, ), ], ); /** * Checkpoint = optimizare (de unde reia scanarea), NU garantia de corectitudine. * Corectitudinea vine din projection_processed_events: chiar daca checkpointul * e in urma si se rescaneaza, evenimentele deja procesate sunt sarite. */ export const projectionCheckpoints = pgTable( 'projection_checkpoints', { id: uuid('id').defaultRandom().primaryKey(), projectionName: text('projection_name').notNull(), projectionVersion: integer('projection_version').notNull(), lastEventAt: timestamp('last_event_at'), lastEventId: uuid('last_event_id'), /** active | paused | rebuilding | failed */ status: text('status').notNull().default('active'), lastError: text('last_error'), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, (table) => [ uniqueIndex('projection_checkpoint_uq').on(table.projectionName, table.projectionVersion), ], ); export const executiveDashboardProjection = pgTable( 'executive_dashboard_projection', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id').notNull(), openTasksCount: integer('open_tasks_count').notNull().default(0), overdueTasksCount: integer('overdue_tasks_count').notNull().default(0), deadlinesNext7Days: integer('deadlines_next_7_days').notNull().default(0), organizationsCount: integer('organizations_count').notNull().default(0), segmentsCount: integer('segments_count').notNull().default(0), researchBriefsCount: integer('research_briefs_count').notNull().default(0), // Campurile de mai jos exista in spec dar depind de module neconstruite // (Documents, Transactions, Approvals, Opportunities). Raman 0 explicit, // ca sa nu para date reale cand modulele nu exista. documentsMissingCount: integer('documents_missing_count').notNull().default(0), transactionsUnclassifiedCount: integer('transactions_unclassified_count').notNull().default(0), pendingApprovalsCount: integer('pending_approvals_count').notNull().default(0), generatedAt: timestamp('generated_at').defaultNow().notNull(), projectionVersion: integer('projection_version').notNull().default(1), }, (table) => [ uniqueIndex('executive_dashboard_tenant_ws_uq').on(table.tenantId, table.workspaceId), ], ); // --- Notifications Engine (spec sectiunile 8-15) --------------------------- // Regula centrala: NU o notificare per eveniment. Motorul decide daca merita // notificat, cui, ce severitate, daca se agrega, ce canal si cand. export const notificationSeverity = pgEnum('notification_severity', [ 'CRITICAL', 'ACTION_REQUIRED', 'WARNING', 'INFORMATION', 'INTELLIGENCE', 'SYSTEM', ]); export const notificationStatus = pgEnum('notification_status', [ 'PENDING', 'SCHEDULED', 'DELIVERING', 'DELIVERED', 'READ', 'ACKNOWLEDGED', 'DISMISSED', 'FAILED', 'EXPIRED', 'ESCALATED', ]); export const notifications = pgTable( 'notifications', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), recipientUserId: uuid('recipient_user_id').notNull(), category: text('category').notNull(), severity: notificationSeverity('severity').notNull(), title: text('title').notNull(), body: text('body'), /** Fiecare notificare are eveniment-sursa (criteriu de acceptare #1). */ sourceEventId: uuid('source_event_id'), sourceEntityType: text('source_entity_type'), sourceEntityId: text('source_entity_id'), actionUrl: text('action_url'), /** Quick action: ce command executa, nu doar marcheaza notificarea. */ actionType: text('action_type'), status: notificationStatus('status').notNull().default('PENDING'), /** tenant + recipient + categorie + entitate: impiedica retrimiterea. */ deduplicationKey: text('deduplication_key').notNull(), /** Mai multe evenimente similare intr-o singura notificare. */ aggregationKey: text('aggregation_key'), aggregatedCount: integer('aggregated_count').notNull().default(1), channels: jsonb('channels').notNull().default(['in_app']), scheduledAt: timestamp('scheduled_at'), expiresAt: timestamp('expires_at'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), readAt: timestamp('read_at'), acknowledgedAt: timestamp('acknowledged_at'), snoozedUntil: timestamp('snoozed_until'), metadata: jsonb('metadata').notNull().default({}), }, (table) => [ index('notifications_recipient_idx').on(table.recipientUserId, table.status), index('notifications_tenant_idx').on(table.tenantId, table.createdAt), index('notifications_dedup_idx').on(table.deduplicationKey, table.createdAt), ], ); export const notificationPreferences = pgTable( 'notification_preferences', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), userId: uuid('user_id').notNull(), /** NULL = se aplica tuturor categoriilor. */ category: text('category'), enabled: integer('enabled').notNull().default(1), minimumSeverity: notificationSeverity('minimum_severity').notNull().default('INFORMATION'), /** Ore locale 0-23. Egale = fara quiet hours. */ quietHoursStart: integer('quiet_hours_start'), quietHoursEnd: integer('quiet_hours_end'), /** CRITICAL poate ignora quiet hours doar daca politica permite. */ criticalBypassesQuietHours: integer('critical_bypasses_quiet_hours').notNull().default(1), timezone: text('timezone').notNull().default('Europe/Bucharest'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, (table) => [uniqueIndex('notification_prefs_uq').on(table.userId, table.tenantId, table.category)], ); /** Idempotency: acelasi eveniment nu produce notificari duplicate. */ export const notificationProcessedEvents = pgTable( 'notification_processed_events', { id: uuid('id').defaultRandom().primaryKey(), ruleId: text('rule_id').notNull(), eventId: uuid('event_id').notNull(), processedAt: timestamp('processed_at').defaultNow().notNull(), }, (table) => [uniqueIndex('notification_processed_uq').on(table.ruleId, table.eventId)], ); // --- Saga Manager (spec Sprint 4-6) --------------------------------------- // O saga coordoneaza un proces cu mai multi pasi care NU incap intr-o singura // tranzactie (apeluri externe, latenta, pasi care pot esua independent). // Garantia nu e atomicitatea, ci: ori toti pasii reusesc, ori cei deja executati // sunt compensati in ordine inversa. export const sagaStatus = pgEnum('saga_status', [ 'RUNNING', 'COMPLETED', 'COMPENSATING', 'COMPENSATED', 'FAILED', 'TIMED_OUT', ]); export const sagaStepStatus = pgEnum('saga_step_status', [ 'PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'COMPENSATED', 'SKIPPED', ]); export const sagaInstances = pgTable( 'saga_instances', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), sagaName: text('saga_name').notNull(), sagaVersion: integer('saga_version').notNull(), status: sagaStatus('status').notNull().default('RUNNING'), /** Indexul pasului curent in definitie. */ currentStep: integer('current_step').notNull().default(0), /** Evenimentul care a pornit saga -- baza idempotentei la start. */ triggerEventId: uuid('trigger_event_id').notNull(), correlationId: uuid('correlation_id'), /** Starea acumulata, citita si scrisa de pasi. */ context: jsonb('context').notNull().default({}), attempt: integer('attempt').notNull().default(0), /** Cand poate fi reluata: backoff exponential dupa esec. */ nextAttemptAt: timestamp('next_attempt_at').defaultNow().notNull(), /** Termen absolut pentru intreaga saga, nu doar pentru pasul curent. */ timeoutAt: timestamp('timeout_at'), lastError: text('last_error'), /** * Lease de executie. Doua procese runner nu trebuie sa avanseze aceeasi * saga simultan; randul se ia cu FOR UPDATE SKIP LOCKED, iar lockedUntil * elibereaza saga daca procesul care o tinea a murit. */ lockedUntil: timestamp('locked_until'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), completedAt: timestamp('completed_at'), }, (table) => [ // Un eveniment porneste o singura instanta dintr-o saga data. uniqueIndex('saga_trigger_uq').on(table.sagaName, table.sagaVersion, table.triggerEventId), index('saga_due_idx').on(table.status, table.nextAttemptAt), index('saga_tenant_idx').on(table.tenantId, table.createdAt), ], ); export const sagaSteps = pgTable( 'saga_steps', { id: uuid('id').defaultRandom().primaryKey(), sagaInstanceId: uuid('saga_instance_id').notNull(), stepName: text('step_name').notNull(), /** Pozitia in definitie; determina si ordinea inversa a compensarii. */ sequence: integer('sequence').notNull(), status: sagaStepStatus('status').notNull().default('PENDING'), attempts: integer('attempts').notNull().default(0), output: jsonb('output'), lastError: text('last_error'), startedAt: timestamp('started_at'), completedAt: timestamp('completed_at'), compensatedAt: timestamp('compensated_at'), }, (table) => [ // Un pas apare o singura data per instanta: baza idempotentei la avans. uniqueIndex('saga_step_uq').on(table.sagaInstanceId, table.sequence), ], ); // Append-oriented (spec sectiunea 29): se scrie, nu se modifica si nu se sterge. export const auditLog = pgTable( 'audit_log', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), /** user | agent | automation | connector | system */ actorType: text('actor_type').notNull().default('user'), actorId: uuid('actor_id'), action: text('action').notNull(), resource: text('resource').notNull(), reason: text('reason'), /** Scopul declarat al operatiunii -- necesar pentru purpose limitation. */ purpose: text('purpose'), changedFields: jsonb('changed_fields'), beforeHash: text('before_hash'), afterHash: text('after_hash'), sessionId: text('session_id'), correlationId: uuid('correlation_id'), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => [index('audit_tenant_created_idx').on(table.tenantId, table.createdAt)], ); // --- Intelligence Engine / Faza A2 (blueprint sectiunea 10.3, 17 "Lead Intelligence") --- // Apollo/ClickHouse raman sursa de adevar pentru datele B2B (blueprint 9: "Private data != // B2B intelligence"); aici pastram doar referinte (organization_id extern) si curatoria // tenantului -- cautari salvate si dovezi de research, nu o copie a datasetului. export const savedSegments = pgTable('saved_segments', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), createdByUserId: uuid('created_by_user_id').notNull(), name: text('name').notNull(), query: text('query').notNull(), resultLimit: integer('result_limit').notNull().default(20), createdAt: timestamp('created_at').defaultNow().notNull(), }); // summary e curatat manual de user in absenta AI Gateway-ului (blueprint sectiunea 14); // schema e gandita ca AI Gateway sa poata popula acelasi camp mai tarziu fara migrare export const researchBriefs = pgTable('research_briefs', { id: uuid('id').defaultRandom().primaryKey(), tenantId: uuid('tenant_id').notNull(), workspaceId: uuid('workspace_id'), createdByUserId: uuid('created_by_user_id').notNull(), organizationId: text('organization_id').notNull(), organizationName: text('organization_name'), title: text('title').notNull(), summary: text('summary').notNull(), sources: jsonb('sources').notNull().default([]), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), });