@danielcok17/prisma-db 1.20.24 → 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
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
|
|
291
|
+
id String @id @default(cuid())
|
|
292
292
|
name String
|
|
293
293
|
userId String
|
|
294
|
-
isShareable Boolean
|
|
295
|
-
shareUrl String?
|
|
294
|
+
isShareable Boolean @default(false)
|
|
295
|
+
shareUrl String? @unique
|
|
296
296
|
sharedAt DateTime?
|
|
297
|
-
createdAt DateTime
|
|
298
|
-
updatedAt DateTime
|
|
297
|
+
createdAt DateTime @default(now())
|
|
298
|
+
updatedAt DateTime @updatedAt
|
|
299
299
|
summary String?
|
|
300
|
-
messagesSinceLastSummary Int?
|
|
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
|
|
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
|
|
349
|
-
answerId
|
|
350
|
-
fileName
|
|
351
|
-
fileType
|
|
352
|
-
base64Data
|
|
353
|
-
convertedBase64Data
|
|
354
|
-
convertedFileName
|
|
355
|
-
convertedFileType
|
|
356
|
-
extractedText
|
|
357
|
-
positionData
|
|
358
|
-
uploadedAt
|
|
359
|
-
answer
|
|
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
|
|
943
|
-
CHAT
|
|
944
|
-
STORAGE
|
|
945
|
-
CANVAS
|
|
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 {
|
|
@@ -955,12 +964,12 @@ enum IndexingJobStatus {
|
|
|
955
964
|
// Paid AI operations on customer files (indexing + user-file search), tracked
|
|
956
965
|
// for an admin-only cost overview.
|
|
957
966
|
enum FileOperationType {
|
|
958
|
-
OCR
|
|
959
|
-
EMBEDDING
|
|
960
|
-
CARD_GEN
|
|
961
|
-
SEARCH_EMBEDDING
|
|
962
|
-
RERANK
|
|
963
|
-
ANSWER
|
|
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
|
|
964
973
|
}
|
|
965
974
|
|
|
966
975
|
enum FolderType {
|
|
@@ -979,13 +988,13 @@ enum FolderPermission {
|
|
|
979
988
|
// Nový model pre logovanie admin akcií
|
|
980
989
|
model AdminActionLog {
|
|
981
990
|
id String @id @default(cuid())
|
|
982
|
-
action String
|
|
983
|
-
targetUserId String
|
|
984
|
-
adminEmail String
|
|
985
|
-
details Json?
|
|
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.)
|
|
986
995
|
createdAt DateTime @default(now())
|
|
987
996
|
// Correlation + security audit fields
|
|
988
|
-
requestId String?
|
|
997
|
+
requestId String? // korelácia s Pino logom (AsyncLocalStorage requestId)
|
|
989
998
|
ipAddress String?
|
|
990
999
|
userAgent String?
|
|
991
1000
|
brand String @default("smartlex")
|
|
@@ -1003,22 +1012,22 @@ model AdminActionLog {
|
|
|
1003
1012
|
// GDPR Article 30 — data access audit trail
|
|
1004
1013
|
// WHO accessed WHAT legal data, WHEN, and from WHERE
|
|
1005
1014
|
model AuditLog {
|
|
1006
|
-
id
|
|
1015
|
+
id String @id @default(cuid())
|
|
1007
1016
|
|
|
1008
1017
|
userId String?
|
|
1009
1018
|
adminEmail String?
|
|
1010
1019
|
|
|
1011
|
-
action String
|
|
1012
|
-
entityType String
|
|
1020
|
+
action String // READ_CONVERSATION | DELETE_ANSWER | EXPORT_DATA | VIEW_USER | SHARE_FOLDER
|
|
1021
|
+
entityType String // "Conversation" | "Answer" | "UserFile" | "User" | "Folder"
|
|
1013
1022
|
entityId String
|
|
1014
|
-
brand String
|
|
1023
|
+
brand String @default("smartlex")
|
|
1015
1024
|
|
|
1016
|
-
requestId
|
|
1017
|
-
ipAddress
|
|
1018
|
-
userAgent
|
|
1019
|
-
metadata
|
|
1025
|
+
requestId String?
|
|
1026
|
+
ipAddress String?
|
|
1027
|
+
userAgent String?
|
|
1028
|
+
metadata Json?
|
|
1020
1029
|
|
|
1021
|
-
createdAt
|
|
1030
|
+
createdAt DateTime @default(now())
|
|
1022
1031
|
|
|
1023
1032
|
@@index([userId])
|
|
1024
1033
|
@@index([action])
|
|
@@ -1031,7 +1040,7 @@ model AuditLog {
|
|
|
1031
1040
|
// Daily aggregated usage — billing analytics, cost trending
|
|
1032
1041
|
// Populated by nightly cron job from AnswerMetrics — never write from request path
|
|
1033
1042
|
model UsageDaily {
|
|
1034
|
-
id
|
|
1043
|
+
id String @id @default(cuid())
|
|
1035
1044
|
|
|
1036
1045
|
date DateTime @db.Date
|
|
1037
1046
|
userId String?
|
|
@@ -1040,16 +1049,16 @@ model UsageDaily {
|
|
|
1040
1049
|
aiProvider String
|
|
1041
1050
|
aiModel String
|
|
1042
1051
|
|
|
1043
|
-
messageCount
|
|
1044
|
-
tokenCount
|
|
1045
|
-
totalCost
|
|
1046
|
-
aiCost
|
|
1047
|
-
ragCost
|
|
1048
|
-
avgDurationMs
|
|
1049
|
-
errorCount
|
|
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)
|
|
1050
1059
|
|
|
1051
|
-
createdAt
|
|
1052
|
-
updatedAt
|
|
1060
|
+
createdAt DateTime @default(now())
|
|
1061
|
+
updatedAt DateTime @updatedAt
|
|
1053
1062
|
|
|
1054
1063
|
@@unique([date, userId, organizationId, aiModel])
|
|
1055
1064
|
@@index([date])
|
|
@@ -1282,44 +1291,44 @@ enum IngestionStatus {
|
|
|
1282
1291
|
}
|
|
1283
1292
|
|
|
1284
1293
|
model UserFile {
|
|
1285
|
-
id
|
|
1286
|
-
userId
|
|
1294
|
+
id String @id @default(cuid())
|
|
1295
|
+
userId String
|
|
1287
1296
|
|
|
1288
|
-
storageKey
|
|
1289
|
-
storageUrl
|
|
1290
|
-
checksumSha256
|
|
1297
|
+
storageKey String @unique
|
|
1298
|
+
storageUrl String?
|
|
1299
|
+
checksumSha256 String
|
|
1291
1300
|
|
|
1292
|
-
fileName
|
|
1293
|
-
fileType
|
|
1294
|
-
fileSize
|
|
1295
|
-
pageCount
|
|
1296
|
-
language
|
|
1301
|
+
fileName String
|
|
1302
|
+
fileType String
|
|
1303
|
+
fileSize Int
|
|
1304
|
+
pageCount Int?
|
|
1305
|
+
language String? @default("sk")
|
|
1297
1306
|
|
|
1298
|
-
status
|
|
1307
|
+
status UserFileStatus @default(STORED)
|
|
1299
1308
|
|
|
1300
|
-
docType
|
|
1301
|
-
legalArea
|
|
1302
|
-
summary
|
|
1303
|
-
extractedData
|
|
1304
|
-
references
|
|
1305
|
-
chunkCount
|
|
1306
|
-
qdrantDocId
|
|
1307
|
-
indexingCost
|
|
1308
|
-
indexedAt
|
|
1309
|
-
indexError
|
|
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?
|
|
1310
1319
|
|
|
1311
|
-
tags
|
|
1320
|
+
tags String[] @default([])
|
|
1312
1321
|
|
|
1313
|
-
parentFileId
|
|
1314
|
-
versionNumber
|
|
1322
|
+
parentFileId String?
|
|
1323
|
+
versionNumber Int @default(1)
|
|
1315
1324
|
|
|
1316
1325
|
uploadSource UserFileUploadSource @default(STORAGE)
|
|
1317
|
-
uploadContextId String?
|
|
1326
|
+
uploadContextId String? // polymorphic: folderId | conversationId | documentId | null
|
|
1318
1327
|
isAiGenerated Boolean @default(false) // true = AI vygeneroval obsah (canvas export)
|
|
1319
1328
|
|
|
1320
|
-
createdAt
|
|
1321
|
-
updatedAt
|
|
1322
|
-
deletedAt
|
|
1329
|
+
createdAt DateTime @default(now())
|
|
1330
|
+
updatedAt DateTime @updatedAt
|
|
1331
|
+
deletedAt DateTime?
|
|
1323
1332
|
|
|
1324
1333
|
user User @relation("UserFiles", fields: [userId], references: [id], onDelete: Cascade)
|
|
1325
1334
|
indexingJobs FileIndexingJob[]
|
|
@@ -1340,13 +1349,13 @@ model UserFile {
|
|
|
1340
1349
|
}
|
|
1341
1350
|
|
|
1342
1351
|
model UserFileConversation {
|
|
1343
|
-
id String
|
|
1352
|
+
id String @id @default(cuid())
|
|
1344
1353
|
fileId String
|
|
1345
1354
|
conversationId String
|
|
1346
|
-
createdAt DateTime
|
|
1355
|
+
createdAt DateTime @default(now())
|
|
1347
1356
|
|
|
1348
|
-
file
|
|
1349
|
-
conversation
|
|
1357
|
+
file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
|
|
1358
|
+
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
1350
1359
|
|
|
1351
1360
|
@@unique([fileId, conversationId])
|
|
1352
1361
|
@@index([fileId])
|
|
@@ -1354,16 +1363,16 @@ model UserFileConversation {
|
|
|
1354
1363
|
}
|
|
1355
1364
|
|
|
1356
1365
|
model UserFileSummaryCard {
|
|
1357
|
-
id
|
|
1358
|
-
fileId
|
|
1359
|
-
userId
|
|
1366
|
+
id String @id @default(cuid())
|
|
1367
|
+
fileId String
|
|
1368
|
+
userId String
|
|
1360
1369
|
|
|
1361
|
-
schemaVersion
|
|
1362
|
-
cardId
|
|
1363
|
-
cardType
|
|
1364
|
-
title
|
|
1365
|
-
summary
|
|
1366
|
-
searchText
|
|
1370
|
+
schemaVersion String @default("user_file_summary_card_v1")
|
|
1371
|
+
cardId String
|
|
1372
|
+
cardType String
|
|
1373
|
+
title String
|
|
1374
|
+
summary String
|
|
1375
|
+
searchText String
|
|
1367
1376
|
|
|
1368
1377
|
normalizedEntities Json?
|
|
1369
1378
|
sourceRefs Json
|
|
@@ -1372,11 +1381,11 @@ model UserFileSummaryCard {
|
|
|
1372
1381
|
confidence Float?
|
|
1373
1382
|
warnings Json?
|
|
1374
1383
|
|
|
1375
|
-
qdrantPointId
|
|
1376
|
-
createdAt
|
|
1377
|
-
updatedAt
|
|
1384
|
+
qdrantPointId String @unique
|
|
1385
|
+
createdAt DateTime @default(now())
|
|
1386
|
+
updatedAt DateTime @updatedAt
|
|
1378
1387
|
|
|
1379
|
-
file
|
|
1388
|
+
file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
|
|
1380
1389
|
|
|
1381
1390
|
@@unique([fileId, cardId])
|
|
1382
1391
|
@@index([fileId])
|
|
@@ -1396,7 +1405,7 @@ model FileIndexingJob {
|
|
|
1396
1405
|
finishedAt DateTime?
|
|
1397
1406
|
createdAt DateTime @default(now())
|
|
1398
1407
|
|
|
1399
|
-
file
|
|
1408
|
+
file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
|
|
1400
1409
|
|
|
1401
1410
|
@@index([fileId])
|
|
1402
1411
|
@@index([userId])
|
|
@@ -1407,26 +1416,26 @@ model FileIndexingJob {
|
|
|
1407
1416
|
// Append-only ledger of paid AI operations on customer files. Scalar fileId/
|
|
1408
1417
|
// userId (no relations) so cost records survive file/user deletion for accounting.
|
|
1409
1418
|
model FileOperationCost {
|
|
1410
|
-
id String
|
|
1419
|
+
id String @id @default(cuid())
|
|
1411
1420
|
userId String
|
|
1412
|
-
fileId String?
|
|
1413
|
-
conversationId String?
|
|
1421
|
+
fileId String? // null for search/answer ops not tied to one file
|
|
1422
|
+
conversationId String? // set for SEARCH_EMBEDDING / RERANK / ANSWER
|
|
1414
1423
|
|
|
1415
|
-
operationType
|
|
1416
|
-
provider
|
|
1417
|
-
model
|
|
1424
|
+
operationType FileOperationType
|
|
1425
|
+
provider String // openai | mistral | google | gemini | cohere
|
|
1426
|
+
model String? // e.g. text-embedding-3-large, mistral-ocr-latest
|
|
1418
1427
|
|
|
1419
|
-
costUsd
|
|
1420
|
-
estimated
|
|
1428
|
+
costUsd Float @default(0)
|
|
1429
|
+
estimated Boolean @default(true) // false = actual usage from provider
|
|
1421
1430
|
|
|
1422
|
-
inputTokens
|
|
1423
|
-
outputTokens
|
|
1424
|
-
totalTokens
|
|
1425
|
-
pages
|
|
1426
|
-
units
|
|
1427
|
-
durationMs
|
|
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?
|
|
1428
1437
|
|
|
1429
|
-
createdAt
|
|
1438
|
+
createdAt DateTime @default(now())
|
|
1430
1439
|
|
|
1431
1440
|
@@index([userId])
|
|
1432
1441
|
@@index([fileId])
|
|
@@ -1448,7 +1457,7 @@ model FileDeadline {
|
|
|
1448
1457
|
sourcePage Int?
|
|
1449
1458
|
createdAt DateTime @default(now())
|
|
1450
1459
|
|
|
1451
|
-
file
|
|
1460
|
+
file UserFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
|
|
1452
1461
|
|
|
1453
1462
|
@@index([fileId])
|
|
1454
1463
|
@@index([userId])
|
|
@@ -1469,7 +1478,7 @@ model FileReviewTable {
|
|
|
1469
1478
|
createdAt DateTime @default(now())
|
|
1470
1479
|
updatedAt DateTime @updatedAt
|
|
1471
1480
|
|
|
1472
|
-
user
|
|
1481
|
+
user User @relation("UserReviewTables", fields: [userId], references: [id], onDelete: Cascade)
|
|
1473
1482
|
|
|
1474
1483
|
@@index([userId])
|
|
1475
1484
|
@@index([folderId])
|
|
@@ -1783,7 +1792,7 @@ model AdminGrant {
|
|
|
1783
1792
|
isActive Boolean @default(true)
|
|
1784
1793
|
revokedAt DateTime?
|
|
1785
1794
|
revokedBy String?
|
|
1786
|
-
invoiceNumber String?
|
|
1795
|
+
invoiceNumber String? // Číslo faktúry pri platenom grante (prevod/faktúra)
|
|
1787
1796
|
createdAt DateTime @default(now())
|
|
1788
1797
|
updatedAt DateTime @updatedAt
|
|
1789
1798
|
|
|
@@ -2074,3 +2083,104 @@ model Referral {
|
|
|
2074
2083
|
@@index([referralCode])
|
|
2075
2084
|
@@index([status])
|
|
2076
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,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
|
+
|