ceo-api/src/db/schema.ts

624 lines
26 KiB
TypeScript

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(),
});