@danielcok17/prisma-db 1.20.23 → 1.20.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielcok17/prisma-db",
3
- "version": "1.20.23",
3
+ "version": "1.20.25",
4
4
  "description": "Shared Prisma schema for Legal AI applications",
5
5
  "repository": {
6
6
  "type": "git",
package/prisma/app.prisma CHANGED
@@ -288,20 +288,20 @@ enum MemberRole {
288
288
 
289
289
  // ZJEDNODUŠENÝ Conversation - len metadata, bez duplikátov
290
290
  model Conversation {
291
- id String @id @default(cuid())
291
+ id String @id @default(cuid())
292
292
  name String
293
293
  userId String
294
- isShareable Boolean @default(false)
295
- shareUrl String? @unique
294
+ isShareable Boolean @default(false)
295
+ shareUrl String? @unique
296
296
  sharedAt DateTime?
297
- createdAt DateTime @default(now())
298
- updatedAt DateTime @updatedAt
297
+ createdAt DateTime @default(now())
298
+ updatedAt DateTime @updatedAt
299
299
  summary String?
300
- messagesSinceLastSummary Int? @default(0)
300
+ messagesSinceLastSummary Int? @default(0)
301
301
  integrityHash String? // HMAC-SHA256 hash chain - posledný chainHash pre verifikáciu integrity
302
302
  // Relácie
303
303
  answers Answer[]
304
- user User @relation(fields: [userId], references: [id])
304
+ user User @relation(fields: [userId], references: [id])
305
305
  workflowLogs WorkflowLog[]
306
306
  canvasDocuments CanvasDocument[]
307
307
  fileUsages UserFileConversation[]
@@ -337,6 +337,7 @@ model Answer {
337
337
  WorkflowLog WorkflowLog[]
338
338
  files MessageFile[]
339
339
  canvasDocumentId String? // No FK constraint - flexible document management
340
+
340
341
  @@index([conversationId])
341
342
  @@index([messageId])
342
343
  @@index([role])
@@ -345,18 +346,18 @@ model Answer {
345
346
  }
346
347
 
347
348
  model MessageFile {
348
- id String @id @default(cuid())
349
- answerId String?
350
- fileName String
351
- fileType String
352
- base64Data String
353
- convertedBase64Data String?
354
- convertedFileName String?
355
- convertedFileType String?
356
- extractedText String?
357
- positionData Json? // Serialized TextPosition[] for coordinate-based citation highlighting
358
- uploadedAt DateTime @default(now())
359
- answer Answer? @relation(fields: [answerId], references: [id], onDelete: Cascade)
349
+ id String @id @default(cuid())
350
+ answerId String?
351
+ fileName String
352
+ fileType String
353
+ base64Data String
354
+ convertedBase64Data String?
355
+ convertedFileName String?
356
+ convertedFileType String?
357
+ extractedText String?
358
+ positionData Json? // Serialized TextPosition[] for coordinate-based citation highlighting
359
+ uploadedAt DateTime @default(now())
360
+ answer Answer? @relation(fields: [answerId], references: [id], onDelete: Cascade)
360
361
 
361
362
  @@index([answerId])
362
363
  @@index([fileType])
@@ -565,6 +566,14 @@ model TouchPoint {
565
566
  country String? // "SK", "CZ", "US"
566
567
  city String? // "Bratislava", "Prague"
567
568
 
569
+ // Ad-click identifiers (parsed today but never persisted — needed for offline
570
+ // conversion upload to Google/Meta/Microsoft/LinkedIn). All nullable/additive.
571
+ gclid String? // Google Ads click id
572
+ fbclid String? // Meta/Facebook click id
573
+ msclkid String? // Microsoft Ads click id
574
+ liFatId String? // LinkedIn click id
575
+ landingUrl String? // First landing URL of the session
576
+
568
577
  // Timestamp
569
578
  timestamp DateTime @default(now())
570
579
 
@@ -939,10 +948,10 @@ enum UserFileStatus {
939
948
  }
940
949
 
941
950
  enum UserFileUploadSource {
942
- FOLDER // nahraté cez UI priečinka / prípadu
943
- CHAT // nahraté z chatovej konverzácie
944
- STORAGE // nahraté priamo do "Môj archív"
945
- CANVAS // AI-vygenerovaný dokument uložený z canvas editora
951
+ FOLDER // nahraté cez UI priečinka / prípadu
952
+ CHAT // nahraté z chatovej konverzácie
953
+ STORAGE // nahraté priamo do "Môj archív"
954
+ CANVAS // AI-vygenerovaný dokument uložený z canvas editora
946
955
  }
947
956
 
948
957
  enum IndexingJobStatus {
@@ -952,6 +961,17 @@ enum IndexingJobStatus {
952
961
  FAILED
953
962
  }
954
963
 
964
+ // Paid AI operations on customer files (indexing + user-file search), tracked
965
+ // for an admin-only cost overview.
966
+ enum FileOperationType {
967
+ OCR // text extraction from images / scanned PDFs
968
+ EMBEDDING // vector embedding of summary cards at index time
969
+ CARD_GEN // LLM summary-card generation
970
+ SEARCH_EMBEDDING // query embedding during user-file search
971
+ RERANK // reranker call during user-file search
972
+ ANSWER // LLM answer over user files
973
+ }
974
+
955
975
  enum FolderType {
956
976
  PERSONAL
957
977
  MATTER
@@ -968,13 +988,13 @@ enum FolderPermission {
968
988
  // Nový model pre logovanie admin akcií
969
989
  model AdminActionLog {
970
990
  id String @id @default(cuid())
971
- action String // APPROVE_USER, REJECT_USER, etc.
972
- targetUserId String // ID používateľa, na ktorého sa akcia vzťahuje
973
- adminEmail String // E-mail admina, ktorý vykonal akciu
974
- details Json? // Ďalšie detaily akcie (reason, notes, etc.)
991
+ action String // APPROVE_USER, REJECT_USER, etc.
992
+ targetUserId String // ID používateľa, na ktorého sa akcia vzťahuje
993
+ adminEmail String // E-mail admina, ktorý vykonal akciu
994
+ details Json? // Ďalšie detaily akcie (reason, notes, etc.)
975
995
  createdAt DateTime @default(now())
976
996
  // Correlation + security audit fields
977
- requestId String? // korelácia s Pino logom (AsyncLocalStorage requestId)
997
+ requestId String? // korelácia s Pino logom (AsyncLocalStorage requestId)
978
998
  ipAddress String?
979
999
  userAgent String?
980
1000
  brand String @default("smartlex")
@@ -992,22 +1012,22 @@ model AdminActionLog {
992
1012
  // GDPR Article 30 — data access audit trail
993
1013
  // WHO accessed WHAT legal data, WHEN, and from WHERE
994
1014
  model AuditLog {
995
- id String @id @default(cuid())
1015
+ id String @id @default(cuid())
996
1016
 
997
1017
  userId String?
998
1018
  adminEmail String?
999
1019
 
1000
- action String // READ_CONVERSATION | DELETE_ANSWER | EXPORT_DATA | VIEW_USER | SHARE_FOLDER
1001
- entityType String // "Conversation" | "Answer" | "UserFile" | "User" | "Folder"
1020
+ action String // READ_CONVERSATION | DELETE_ANSWER | EXPORT_DATA | VIEW_USER | SHARE_FOLDER
1021
+ entityType String // "Conversation" | "Answer" | "UserFile" | "User" | "Folder"
1002
1022
  entityId String
1003
- brand String @default("smartlex")
1023
+ brand String @default("smartlex")
1004
1024
 
1005
- requestId String?
1006
- ipAddress String?
1007
- userAgent String?
1008
- metadata Json?
1025
+ requestId String?
1026
+ ipAddress String?
1027
+ userAgent String?
1028
+ metadata Json?
1009
1029
 
1010
- createdAt DateTime @default(now())
1030
+ createdAt DateTime @default(now())
1011
1031
 
1012
1032
  @@index([userId])
1013
1033
  @@index([action])
@@ -1020,7 +1040,7 @@ model AuditLog {
1020
1040
  // Daily aggregated usage — billing analytics, cost trending
1021
1041
  // Populated by nightly cron job from AnswerMetrics — never write from request path
1022
1042
  model UsageDaily {
1023
- id String @id @default(cuid())
1043
+ id String @id @default(cuid())
1024
1044
 
1025
1045
  date DateTime @db.Date
1026
1046
  userId String?
@@ -1029,16 +1049,16 @@ model UsageDaily {
1029
1049
  aiProvider String
1030
1050
  aiModel String
1031
1051
 
1032
- messageCount Int @default(0)
1033
- tokenCount Int @default(0)
1034
- totalCost Float @default(0)
1035
- aiCost Float @default(0)
1036
- ragCost Float @default(0)
1037
- avgDurationMs Float?
1038
- errorCount Int @default(0)
1052
+ messageCount Int @default(0)
1053
+ tokenCount Int @default(0)
1054
+ totalCost Float @default(0)
1055
+ aiCost Float @default(0)
1056
+ ragCost Float @default(0)
1057
+ avgDurationMs Float?
1058
+ errorCount Int @default(0)
1039
1059
 
1040
- createdAt DateTime @default(now())
1041
- updatedAt DateTime @updatedAt
1060
+ createdAt DateTime @default(now())
1061
+ updatedAt DateTime @updatedAt
1042
1062
 
1043
1063
  @@unique([date, userId, organizationId, aiModel])
1044
1064
  @@index([date])
@@ -1271,44 +1291,44 @@ enum IngestionStatus {
1271
1291
  }
1272
1292
 
1273
1293
  model UserFile {
1274
- id String @id @default(cuid())
1275
- userId String
1294
+ id String @id @default(cuid())
1295
+ userId String
1276
1296
 
1277
- storageKey String @unique
1278
- storageUrl String?
1279
- checksumSha256 String
1297
+ storageKey String @unique
1298
+ storageUrl String?
1299
+ checksumSha256 String
1280
1300
 
1281
- fileName String
1282
- fileType String
1283
- fileSize Int
1284
- pageCount Int?
1285
- language String? @default("sk")
1301
+ fileName String
1302
+ fileType String
1303
+ fileSize Int
1304
+ pageCount Int?
1305
+ language String? @default("sk")
1286
1306
 
1287
- status UserFileStatus @default(STORED)
1307
+ status UserFileStatus @default(STORED)
1288
1308
 
1289
- docType String?
1290
- legalArea String?
1291
- summary String?
1292
- extractedData Json?
1293
- references String[] @default([])
1294
- chunkCount Int?
1295
- qdrantDocId String? @unique
1296
- indexingCost Float?
1297
- indexedAt DateTime?
1298
- indexError String?
1309
+ docType String?
1310
+ legalArea String?
1311
+ summary String?
1312
+ extractedData Json?
1313
+ references String[] @default([])
1314
+ chunkCount Int?
1315
+ qdrantDocId String? @unique
1316
+ indexingCost Float?
1317
+ indexedAt DateTime?
1318
+ indexError String?
1299
1319
 
1300
- tags String[] @default([])
1320
+ tags String[] @default([])
1301
1321
 
1302
- parentFileId String?
1303
- versionNumber Int @default(1)
1322
+ parentFileId String?
1323
+ versionNumber Int @default(1)
1304
1324
 
1305
1325
  uploadSource UserFileUploadSource @default(STORAGE)
1306
- uploadContextId String? // polymorphic: folderId | conversationId | documentId | null
1326
+ uploadContextId String? // polymorphic: folderId | conversationId | documentId | null
1307
1327
  isAiGenerated Boolean @default(false) // true = AI vygeneroval obsah (canvas export)
1308
1328
 
1309
- createdAt DateTime @default(now())
1310
- updatedAt DateTime @updatedAt
1311
- deletedAt DateTime?
1329
+ createdAt DateTime @default(now())
1330
+ updatedAt DateTime @updatedAt
1331
+ deletedAt DateTime?
1312
1332
 
1313
1333
  user User @relation("UserFiles", fields: [userId], references: [id], onDelete: Cascade)
1314
1334
  indexingJobs FileIndexingJob[]
@@ -1329,13 +1349,13 @@ model UserFile {
1329
1349
  }
1330
1350
 
1331
1351
  model UserFileConversation {
1332
- id String @id @default(cuid())
1352
+ id String @id @default(cuid())
1333
1353
  fileId String
1334
1354
  conversationId String
1335
- createdAt DateTime @default(now())
1355
+ createdAt DateTime @default(now())
1336
1356
 
1337
- file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
1338
- conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
1357
+ file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
1358
+ conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
1339
1359
 
1340
1360
  @@unique([fileId, conversationId])
1341
1361
  @@index([fileId])
@@ -1343,16 +1363,16 @@ model UserFileConversation {
1343
1363
  }
1344
1364
 
1345
1365
  model UserFileSummaryCard {
1346
- id String @id @default(cuid())
1347
- fileId String
1348
- userId String
1366
+ id String @id @default(cuid())
1367
+ fileId String
1368
+ userId String
1349
1369
 
1350
- schemaVersion String @default("user_file_summary_card_v1")
1351
- cardId String
1352
- cardType String
1353
- title String
1354
- summary String
1355
- searchText String
1370
+ schemaVersion String @default("user_file_summary_card_v1")
1371
+ cardId String
1372
+ cardType String
1373
+ title String
1374
+ summary String
1375
+ searchText String
1356
1376
 
1357
1377
  normalizedEntities Json?
1358
1378
  sourceRefs Json
@@ -1361,11 +1381,11 @@ model UserFileSummaryCard {
1361
1381
  confidence Float?
1362
1382
  warnings Json?
1363
1383
 
1364
- qdrantPointId String @unique
1365
- createdAt DateTime @default(now())
1366
- updatedAt DateTime @updatedAt
1384
+ qdrantPointId String @unique
1385
+ createdAt DateTime @default(now())
1386
+ updatedAt DateTime @updatedAt
1367
1387
 
1368
- file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
1388
+ file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
1369
1389
 
1370
1390
  @@unique([fileId, cardId])
1371
1391
  @@index([fileId])
@@ -1385,7 +1405,7 @@ model FileIndexingJob {
1385
1405
  finishedAt DateTime?
1386
1406
  createdAt DateTime @default(now())
1387
1407
 
1388
- file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
1408
+ file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
1389
1409
 
1390
1410
  @@index([fileId])
1391
1411
  @@index([userId])
@@ -1393,6 +1413,37 @@ model FileIndexingJob {
1393
1413
  @@index([createdAt])
1394
1414
  }
1395
1415
 
1416
+ // Append-only ledger of paid AI operations on customer files. Scalar fileId/
1417
+ // userId (no relations) so cost records survive file/user deletion for accounting.
1418
+ model FileOperationCost {
1419
+ id String @id @default(cuid())
1420
+ userId String
1421
+ fileId String? // null for search/answer ops not tied to one file
1422
+ conversationId String? // set for SEARCH_EMBEDDING / RERANK / ANSWER
1423
+
1424
+ operationType FileOperationType
1425
+ provider String // openai | mistral | google | gemini | cohere
1426
+ model String? // e.g. text-embedding-3-large, mistral-ocr-latest
1427
+
1428
+ costUsd Float @default(0)
1429
+ estimated Boolean @default(true) // false = actual usage from provider
1430
+
1431
+ inputTokens Int?
1432
+ outputTokens Int?
1433
+ totalTokens Int?
1434
+ pages Int? // OCR pages
1435
+ units Int? // generic unit count (embedded docs, chars, etc.)
1436
+ durationMs Int?
1437
+
1438
+ createdAt DateTime @default(now())
1439
+
1440
+ @@index([userId])
1441
+ @@index([fileId])
1442
+ @@index([operationType])
1443
+ @@index([createdAt])
1444
+ @@index([userId, createdAt])
1445
+ }
1446
+
1396
1447
  model FileDeadline {
1397
1448
  id String @id @default(cuid())
1398
1449
  fileId String
@@ -1406,7 +1457,7 @@ model FileDeadline {
1406
1457
  sourcePage Int?
1407
1458
  createdAt DateTime @default(now())
1408
1459
 
1409
- file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
1460
+ file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
1410
1461
 
1411
1462
  @@index([fileId])
1412
1463
  @@index([userId])
@@ -1427,7 +1478,7 @@ model FileReviewTable {
1427
1478
  createdAt DateTime @default(now())
1428
1479
  updatedAt DateTime @updatedAt
1429
1480
 
1430
- user User @relation("UserReviewTables", fields: [userId], references: [id], onDelete: Cascade)
1481
+ user User @relation("UserReviewTables", fields: [userId], references: [id], onDelete: Cascade)
1431
1482
 
1432
1483
  @@index([userId])
1433
1484
  @@index([folderId])
@@ -1741,7 +1792,7 @@ model AdminGrant {
1741
1792
  isActive Boolean @default(true)
1742
1793
  revokedAt DateTime?
1743
1794
  revokedBy String?
1744
- invoiceNumber String? // Číslo faktúry pri platenom grante (prevod/faktúra)
1795
+ invoiceNumber String? // Číslo faktúry pri platenom grante (prevod/faktúra)
1745
1796
  createdAt DateTime @default(now())
1746
1797
  updatedAt DateTime @updatedAt
1747
1798
 
@@ -2032,3 +2083,104 @@ model Referral {
2032
2083
  @@index([referralCode])
2033
2084
  @@index([status])
2034
2085
  }
2086
+
2087
+ // ============================================
2088
+ // OUTBOUND ATTRIBUTION (SDR cold calls + Instantly cold email)
2089
+ // See consumer repo: OUTBOUND_RESEARCH_AND_AUDIT.md §5
2090
+ // ============================================
2091
+
2092
+ enum OutboundChannel {
2093
+ CALL
2094
+ EMAIL
2095
+ LINKEDIN
2096
+ EVENT
2097
+ }
2098
+
2099
+ /// A sales rep (SDR) — the attribution dimension previously missing everywhere.
2100
+ model Sdr {
2101
+ id String @id @default(cuid())
2102
+ userId String? @unique // if the SDR also has an app login
2103
+ name String
2104
+ slug String @unique // powers smartlex.sk/<slug> vanity links
2105
+ active Boolean @default(true)
2106
+ createdAt DateTime @default(now())
2107
+
2108
+ codes InviteCode[]
2109
+ touches OutboundTouch[]
2110
+
2111
+ @@index([active])
2112
+ }
2113
+
2114
+ /// One row per deliberate outbound contact: a call, or an Instantly send.
2115
+ model OutboundTouch {
2116
+ id String @id @default(cuid())
2117
+ channel OutboundChannel
2118
+ sdrId String?
2119
+ sdr Sdr? @relation(fields: [sdrId], references: [id], onDelete: SetNull)
2120
+ leadId String? // prospect record id (Instantly lead id, etc.)
2121
+ campaignId String? // Instantly campaign UUID
2122
+ instantlyStep Int?
2123
+ emailHash String? // sha256(normalized email) — pseudonymised, NOT anonymous (ROPA/DSAR)
2124
+ occurredAt DateTime
2125
+ outcome String? // 'connected' | 'voicemail' | 'reply_positive' | ...
2126
+ inviteCodeId String?
2127
+ createdAt DateTime @default(now())
2128
+
2129
+ @@index([sdrId, occurredAt])
2130
+ @@index([campaignId])
2131
+ @@index([emailHash])
2132
+ }
2133
+
2134
+ /// Unguessable, phone-readable invite code (Crockford Base32). See consumer lib/invite-codes.ts.
2135
+ model InviteCode {
2136
+ id String @id @default(cuid())
2137
+ code String @unique // NORMALIZED lookup key (uppercase, no dashes, I/L->1, O->0)
2138
+ display String // "ABCD-EFGH-JKMN-P" as shown to humans
2139
+ campaignId String
2140
+ issuedBySdrId String // the attribution AppInvite.invitedByEmail cannot express
2141
+ sdr Sdr @relation(fields: [issuedBySdrId], references: [id], onDelete: Cascade)
2142
+ issuedToEmail String? // per-prospect; null for shared codes
2143
+ grantTier SubscriptionTier
2144
+ grantDays Int
2145
+ maxRedemptions Int @default(1)
2146
+ usedCount Int @default(0)
2147
+ clickCount Int @default(0)
2148
+ expiresAt DateTime?
2149
+ revokedAt DateTime?
2150
+ createdAt DateTime @default(now())
2151
+
2152
+ redemptions Redemption[]
2153
+
2154
+ @@index([issuedBySdrId])
2155
+ @@index([issuedToEmail])
2156
+ }
2157
+
2158
+ /// One redemption. The unique constraints are the hard backstop against races.
2159
+ model Redemption {
2160
+ id String @id @default(cuid())
2161
+ codeId String
2162
+ code InviteCode @relation(fields: [codeId], references: [id], onDelete: Cascade)
2163
+ userId String
2164
+ emailDomain String
2165
+ sdrId String // denormalized: survives code reassignment
2166
+ campaignId String
2167
+ idempotencyKey String? @unique
2168
+ redeemedAt DateTime @default(now())
2169
+
2170
+ @@unique([codeId, userId]) // hard backstop: a user redeems a given code at most once
2171
+ @@unique([campaignId, emailDomain]) // one grant per company domain
2172
+ @@index([sdrId, redeemedAt])
2173
+ }
2174
+
2175
+ /// One row per attribution SIGNAL (not a verdict) — let reporting pick the threshold.
2176
+ model LeadAttribution {
2177
+ id String @id @default(cuid())
2178
+ userId String
2179
+ leadId String?
2180
+ matchMethod String // 'invite_code'|'email_exact'|'domain'|'fuzzy_name'|'self_report'
2181
+ confidence Decimal @db.Decimal(3, 2)
2182
+ evidence Json @default("{}")
2183
+ createdAt DateTime @default(now())
2184
+
2185
+ @@index([userId])
2186
+ }
@@ -0,0 +1,39 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "FileOperationType" AS ENUM ('OCR', 'EMBEDDING', 'CARD_GEN', 'SEARCH_EMBEDDING', 'RERANK', 'ANSWER');
3
+
4
+ -- CreateTable
5
+ CREATE TABLE "FileOperationCost" (
6
+ "id" TEXT NOT NULL,
7
+ "userId" TEXT NOT NULL,
8
+ "fileId" TEXT,
9
+ "conversationId" TEXT,
10
+ "operationType" "FileOperationType" NOT NULL,
11
+ "provider" TEXT NOT NULL,
12
+ "model" TEXT,
13
+ "costUsd" DOUBLE PRECISION NOT NULL DEFAULT 0,
14
+ "estimated" BOOLEAN NOT NULL DEFAULT true,
15
+ "inputTokens" INTEGER,
16
+ "outputTokens" INTEGER,
17
+ "totalTokens" INTEGER,
18
+ "pages" INTEGER,
19
+ "units" INTEGER,
20
+ "durationMs" INTEGER,
21
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
22
+
23
+ CONSTRAINT "FileOperationCost_pkey" PRIMARY KEY ("id")
24
+ );
25
+
26
+ -- CreateIndex
27
+ CREATE INDEX "FileOperationCost_userId_idx" ON "FileOperationCost"("userId");
28
+
29
+ -- CreateIndex
30
+ CREATE INDEX "FileOperationCost_fileId_idx" ON "FileOperationCost"("fileId");
31
+
32
+ -- CreateIndex
33
+ CREATE INDEX "FileOperationCost_operationType_idx" ON "FileOperationCost"("operationType");
34
+
35
+ -- CreateIndex
36
+ CREATE INDEX "FileOperationCost_createdAt_idx" ON "FileOperationCost"("createdAt");
37
+
38
+ -- CreateIndex
39
+ CREATE INDEX "FileOperationCost_userId_createdAt_idx" ON "FileOperationCost"("userId", "createdAt");
@@ -0,0 +1,137 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "OutboundChannel" AS ENUM ('CALL', 'EMAIL', 'LINKEDIN', 'EVENT');
3
+
4
+ -- AlterTable
5
+ ALTER TABLE "touch_points" ADD COLUMN "fbclid" TEXT,
6
+ ADD COLUMN "gclid" TEXT,
7
+ ADD COLUMN "landingUrl" TEXT,
8
+ ADD COLUMN "liFatId" TEXT,
9
+ ADD COLUMN "msclkid" TEXT;
10
+
11
+ -- CreateTable
12
+ CREATE TABLE "Sdr" (
13
+ "id" TEXT NOT NULL,
14
+ "userId" TEXT,
15
+ "name" TEXT NOT NULL,
16
+ "slug" TEXT NOT NULL,
17
+ "active" BOOLEAN NOT NULL DEFAULT true,
18
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+
20
+ CONSTRAINT "Sdr_pkey" PRIMARY KEY ("id")
21
+ );
22
+
23
+ -- CreateTable
24
+ CREATE TABLE "OutboundTouch" (
25
+ "id" TEXT NOT NULL,
26
+ "channel" "OutboundChannel" NOT NULL,
27
+ "sdrId" TEXT,
28
+ "leadId" TEXT,
29
+ "campaignId" TEXT,
30
+ "instantlyStep" INTEGER,
31
+ "emailHash" TEXT,
32
+ "occurredAt" TIMESTAMP(3) NOT NULL,
33
+ "outcome" TEXT,
34
+ "inviteCodeId" TEXT,
35
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
36
+
37
+ CONSTRAINT "OutboundTouch_pkey" PRIMARY KEY ("id")
38
+ );
39
+
40
+ -- CreateTable
41
+ CREATE TABLE "InviteCode" (
42
+ "id" TEXT NOT NULL,
43
+ "code" TEXT NOT NULL,
44
+ "display" TEXT NOT NULL,
45
+ "campaignId" TEXT NOT NULL,
46
+ "issuedBySdrId" TEXT NOT NULL,
47
+ "issuedToEmail" TEXT,
48
+ "grantTier" "SubscriptionTier" NOT NULL,
49
+ "grantDays" INTEGER NOT NULL,
50
+ "maxRedemptions" INTEGER NOT NULL DEFAULT 1,
51
+ "usedCount" INTEGER NOT NULL DEFAULT 0,
52
+ "clickCount" INTEGER NOT NULL DEFAULT 0,
53
+ "expiresAt" TIMESTAMP(3),
54
+ "revokedAt" TIMESTAMP(3),
55
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
56
+
57
+ CONSTRAINT "InviteCode_pkey" PRIMARY KEY ("id")
58
+ );
59
+
60
+ -- CreateTable
61
+ CREATE TABLE "Redemption" (
62
+ "id" TEXT NOT NULL,
63
+ "codeId" TEXT NOT NULL,
64
+ "userId" TEXT NOT NULL,
65
+ "emailDomain" TEXT NOT NULL,
66
+ "sdrId" TEXT NOT NULL,
67
+ "campaignId" TEXT NOT NULL,
68
+ "idempotencyKey" TEXT,
69
+ "redeemedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
70
+
71
+ CONSTRAINT "Redemption_pkey" PRIMARY KEY ("id")
72
+ );
73
+
74
+ -- CreateTable
75
+ CREATE TABLE "LeadAttribution" (
76
+ "id" TEXT NOT NULL,
77
+ "userId" TEXT NOT NULL,
78
+ "leadId" TEXT,
79
+ "matchMethod" TEXT NOT NULL,
80
+ "confidence" DECIMAL(3,2) NOT NULL,
81
+ "evidence" JSONB NOT NULL DEFAULT '{}',
82
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
83
+
84
+ CONSTRAINT "LeadAttribution_pkey" PRIMARY KEY ("id")
85
+ );
86
+
87
+ -- CreateIndex
88
+ CREATE UNIQUE INDEX "Sdr_userId_key" ON "Sdr"("userId");
89
+
90
+ -- CreateIndex
91
+ CREATE UNIQUE INDEX "Sdr_slug_key" ON "Sdr"("slug");
92
+
93
+ -- CreateIndex
94
+ CREATE INDEX "Sdr_active_idx" ON "Sdr"("active");
95
+
96
+ -- CreateIndex
97
+ CREATE INDEX "OutboundTouch_sdrId_occurredAt_idx" ON "OutboundTouch"("sdrId", "occurredAt");
98
+
99
+ -- CreateIndex
100
+ CREATE INDEX "OutboundTouch_campaignId_idx" ON "OutboundTouch"("campaignId");
101
+
102
+ -- CreateIndex
103
+ CREATE INDEX "OutboundTouch_emailHash_idx" ON "OutboundTouch"("emailHash");
104
+
105
+ -- CreateIndex
106
+ CREATE UNIQUE INDEX "InviteCode_code_key" ON "InviteCode"("code");
107
+
108
+ -- CreateIndex
109
+ CREATE INDEX "InviteCode_issuedBySdrId_idx" ON "InviteCode"("issuedBySdrId");
110
+
111
+ -- CreateIndex
112
+ CREATE INDEX "InviteCode_issuedToEmail_idx" ON "InviteCode"("issuedToEmail");
113
+
114
+ -- CreateIndex
115
+ CREATE UNIQUE INDEX "Redemption_idempotencyKey_key" ON "Redemption"("idempotencyKey");
116
+
117
+ -- CreateIndex
118
+ CREATE INDEX "Redemption_sdrId_redeemedAt_idx" ON "Redemption"("sdrId", "redeemedAt");
119
+
120
+ -- CreateIndex
121
+ CREATE UNIQUE INDEX "Redemption_codeId_userId_key" ON "Redemption"("codeId", "userId");
122
+
123
+ -- CreateIndex
124
+ CREATE UNIQUE INDEX "Redemption_campaignId_emailDomain_key" ON "Redemption"("campaignId", "emailDomain");
125
+
126
+ -- CreateIndex
127
+ CREATE INDEX "LeadAttribution_userId_idx" ON "LeadAttribution"("userId");
128
+
129
+ -- AddForeignKey
130
+ ALTER TABLE "OutboundTouch" ADD CONSTRAINT "OutboundTouch_sdrId_fkey" FOREIGN KEY ("sdrId") REFERENCES "Sdr"("id") ON DELETE SET NULL ON UPDATE CASCADE;
131
+
132
+ -- AddForeignKey
133
+ ALTER TABLE "InviteCode" ADD CONSTRAINT "InviteCode_issuedBySdrId_fkey" FOREIGN KEY ("issuedBySdrId") REFERENCES "Sdr"("id") ON DELETE CASCADE ON UPDATE CASCADE;
134
+
135
+ -- AddForeignKey
136
+ ALTER TABLE "Redemption" ADD CONSTRAINT "Redemption_codeId_fkey" FOREIGN KEY ("codeId") REFERENCES "InviteCode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
137
+