@lead-routing/cli 0.6.8 → 0.7.0
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/dist/index.js
CHANGED
|
@@ -112,6 +112,7 @@ function bail2(value) {
|
|
|
112
112
|
throw new Error("Unexpected cancel");
|
|
113
113
|
}
|
|
114
114
|
async function collectConfig(opts = {}) {
|
|
115
|
+
const crmType = opts.crmType ?? "salesforce";
|
|
115
116
|
note2(
|
|
116
117
|
"You will need:\n \u2022 Public HTTPS URLs for the web app and routing engine",
|
|
117
118
|
"Before you begin"
|
|
@@ -130,14 +131,15 @@ async function collectConfig(opts = {}) {
|
|
|
130
131
|
}
|
|
131
132
|
});
|
|
132
133
|
if (isCancel2(appUrl)) bail2(appUrl);
|
|
134
|
+
const crmLabel = crmType === "hubspot" ? "HubSpot" : "Salesforce";
|
|
133
135
|
const engineUrl = await text2({
|
|
134
|
-
message:
|
|
136
|
+
message: `Engine URL (public URL ${crmLabel} will use to route leads)`,
|
|
135
137
|
placeholder: "https://engine.acme.com or https://acme.com:3001",
|
|
136
138
|
validate: (v) => {
|
|
137
139
|
if (!v) return "Required";
|
|
138
140
|
try {
|
|
139
141
|
const u = new URL(v);
|
|
140
|
-
if (u.protocol !== "https:") return
|
|
142
|
+
if (u.protocol !== "https:") return `Must be an HTTPS URL (${crmLabel} requires HTTPS)`;
|
|
141
143
|
} catch {
|
|
142
144
|
return "Must be a valid URL (e.g. https://engine.acme.com)";
|
|
143
145
|
}
|
|
@@ -171,9 +173,39 @@ async function collectConfig(opts = {}) {
|
|
|
171
173
|
const sessionSecret = generateSecret(32);
|
|
172
174
|
const engineWebhookSecret = generateSecret(32);
|
|
173
175
|
const internalApiKey = generateSecret(32);
|
|
176
|
+
let hubspotAppId;
|
|
177
|
+
let hubspotClientId;
|
|
178
|
+
let hubspotClientSecret;
|
|
179
|
+
if (crmType === "hubspot") {
|
|
180
|
+
note2(
|
|
181
|
+
"Create a HubSpot app at https://developers.hubspot.com/\nYou will need the App ID, Client ID, and Client Secret.",
|
|
182
|
+
"HubSpot Credentials"
|
|
183
|
+
);
|
|
184
|
+
const rawAppId = await text2({
|
|
185
|
+
message: "HubSpot App ID",
|
|
186
|
+
placeholder: "123456",
|
|
187
|
+
validate: (v) => !v?.trim() ? "Required" : void 0
|
|
188
|
+
});
|
|
189
|
+
if (isCancel2(rawAppId)) bail2(rawAppId);
|
|
190
|
+
hubspotAppId = rawAppId.trim();
|
|
191
|
+
const rawClientId = await text2({
|
|
192
|
+
message: "HubSpot Client ID",
|
|
193
|
+
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
194
|
+
validate: (v) => !v?.trim() ? "Required" : void 0
|
|
195
|
+
});
|
|
196
|
+
if (isCancel2(rawClientId)) bail2(rawClientId);
|
|
197
|
+
hubspotClientId = rawClientId.trim();
|
|
198
|
+
const rawClientSecret = await password2({
|
|
199
|
+
message: "HubSpot Client Secret",
|
|
200
|
+
validate: (v) => !v?.trim() ? "Required" : void 0
|
|
201
|
+
});
|
|
202
|
+
if (isCancel2(rawClientSecret)) bail2(rawClientSecret);
|
|
203
|
+
hubspotClientSecret = rawClientSecret.trim();
|
|
204
|
+
}
|
|
174
205
|
return {
|
|
175
206
|
appUrl: appUrl.trim().replace(/\/+$/, ""),
|
|
176
207
|
engineUrl: engineUrl.trim().replace(/\/+$/, ""),
|
|
208
|
+
crmType,
|
|
177
209
|
managedDb,
|
|
178
210
|
databaseUrl,
|
|
179
211
|
dbPassword: managedDb ? dbPassword : "",
|
|
@@ -186,7 +218,10 @@ async function collectConfig(opts = {}) {
|
|
|
186
218
|
feedbackToEmail: "",
|
|
187
219
|
sessionSecret,
|
|
188
220
|
engineWebhookSecret,
|
|
189
|
-
internalApiKey
|
|
221
|
+
internalApiKey,
|
|
222
|
+
hubspotAppId,
|
|
223
|
+
hubspotClientId,
|
|
224
|
+
hubspotClientSecret
|
|
190
225
|
};
|
|
191
226
|
}
|
|
192
227
|
|
|
@@ -347,7 +382,18 @@ function renderEnvWeb(c) {
|
|
|
347
382
|
``,
|
|
348
383
|
`# Email (optional)`,
|
|
349
384
|
`RESEND_API_KEY=${c.resendApiKey ?? ""}`,
|
|
350
|
-
`FEEDBACK_TO_EMAIL=${c.feedbackToEmail ?? ""}
|
|
385
|
+
`FEEDBACK_TO_EMAIL=${c.feedbackToEmail ?? ""}`,
|
|
386
|
+
``,
|
|
387
|
+
`# CRM`,
|
|
388
|
+
`CRM_TYPE=${c.crmType ?? "salesforce"}`,
|
|
389
|
+
...c.crmType === "hubspot" ? [
|
|
390
|
+
``,
|
|
391
|
+
`# HubSpot`,
|
|
392
|
+
`HUBSPOT_CLIENT_ID=${c.hubspotClientId ?? ""}`,
|
|
393
|
+
`HUBSPOT_CLIENT_SECRET=${c.hubspotClientSecret ?? ""}`,
|
|
394
|
+
`HUBSPOT_APP_ID=${c.hubspotAppId ?? ""}`,
|
|
395
|
+
`HUBSPOT_REDIRECT_URI=${c.appUrl}/api/auth/hubspot/callback`
|
|
396
|
+
] : []
|
|
351
397
|
].join("\n");
|
|
352
398
|
}
|
|
353
399
|
|
|
@@ -377,7 +423,12 @@ function renderEnvEngine(c) {
|
|
|
377
423
|
`# License`,
|
|
378
424
|
`LICENSE_KEY=${c.licenseKey ?? ""}`,
|
|
379
425
|
`LICENSE_TIER=${c.licenseTier}`,
|
|
380
|
-
`LICENSE_API_URL=https://lead-routing-license.artyagi2011.workers.dev
|
|
426
|
+
`LICENSE_API_URL=https://lead-routing-license.artyagi2011.workers.dev`,
|
|
427
|
+
...c.crmType === "hubspot" ? [
|
|
428
|
+
``,
|
|
429
|
+
`# HubSpot`,
|
|
430
|
+
`HUBSPOT_CLIENT_SECRET=${c.hubspotClientSecret ?? ""}`
|
|
431
|
+
] : []
|
|
381
432
|
].join("\n");
|
|
382
433
|
}
|
|
383
434
|
|
|
@@ -538,7 +589,11 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
|
|
|
538
589
|
resendApiKey: cfg.resendApiKey || void 0,
|
|
539
590
|
feedbackToEmail: cfg.feedbackToEmail || void 0,
|
|
540
591
|
licenseKey: license.licenseKey,
|
|
541
|
-
licenseTier: license.licenseTier
|
|
592
|
+
licenseTier: license.licenseTier,
|
|
593
|
+
crmType: cfg.crmType,
|
|
594
|
+
hubspotClientId: cfg.hubspotClientId,
|
|
595
|
+
hubspotClientSecret: cfg.hubspotClientSecret,
|
|
596
|
+
hubspotAppId: cfg.hubspotAppId
|
|
542
597
|
});
|
|
543
598
|
const envWeb = join2(dir, ".env.web");
|
|
544
599
|
writeFileSync2(envWeb, envWebContent, "utf8");
|
|
@@ -549,7 +604,9 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
|
|
|
549
604
|
engineWebhookSecret: cfg.engineWebhookSecret,
|
|
550
605
|
internalApiKey: cfg.internalApiKey,
|
|
551
606
|
licenseKey: license.licenseKey,
|
|
552
|
-
licenseTier: license.licenseTier
|
|
607
|
+
licenseTier: license.licenseTier,
|
|
608
|
+
crmType: cfg.crmType,
|
|
609
|
+
hubspotClientSecret: cfg.hubspotClientSecret
|
|
553
610
|
});
|
|
554
611
|
const envEngine = join2(dir, ".env.engine");
|
|
555
612
|
writeFileSync2(envEngine, envEngineContent, "utf8");
|
|
@@ -557,6 +614,7 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
|
|
|
557
614
|
writeConfig(dir, {
|
|
558
615
|
appUrl: cfg.appUrl,
|
|
559
616
|
engineUrl: cfg.engineUrl,
|
|
617
|
+
crmType: cfg.crmType,
|
|
560
618
|
installDir: dir,
|
|
561
619
|
remoteDir: sshCfg.remoteDir,
|
|
562
620
|
ssh: {
|
|
@@ -1315,53 +1373,84 @@ After verifying, press Enter to continue.`,
|
|
|
1315
1373
|
try {
|
|
1316
1374
|
log7.step("Step 1/9 License validation");
|
|
1317
1375
|
const licenseResult = { tier: auth.customer.tier, key: void 0 };
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
"Salesforce Package"
|
|
1325
|
-
);
|
|
1326
|
-
log7.info("Opening install URL in your browser...");
|
|
1327
|
-
openBrowser(MANAGED_PACKAGE_INSTALL_URL);
|
|
1328
|
-
log7.info(`${chalk2.dim("If the browser didn't open, visit the URL above manually.")}`);
|
|
1329
|
-
const installed = await confirm({
|
|
1330
|
-
message: 'Have you installed the package? (Click "Install for All Users" in Salesforce)',
|
|
1331
|
-
initialValue: false
|
|
1376
|
+
const crmChoice = await select({
|
|
1377
|
+
message: "Which CRM will you connect?",
|
|
1378
|
+
options: [
|
|
1379
|
+
{ value: "salesforce", label: "Salesforce" },
|
|
1380
|
+
{ value: "hubspot", label: "HubSpot" }
|
|
1381
|
+
]
|
|
1332
1382
|
});
|
|
1333
|
-
if (isCancel3(
|
|
1383
|
+
if (isCancel3(crmChoice)) {
|
|
1334
1384
|
cancel3("Setup cancelled.");
|
|
1335
1385
|
process.exit(0);
|
|
1336
1386
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1387
|
+
const crmType = crmChoice;
|
|
1388
|
+
if (crmType === "salesforce") {
|
|
1389
|
+
log7.step("Step 2/9 Install Salesforce Package");
|
|
1390
|
+
note3(
|
|
1391
|
+
`The Lead Router managed package installs the required Connected App,
|
|
1392
|
+
triggers, and custom objects in your Salesforce org.
|
|
1393
|
+
|
|
1394
|
+
Install URL: ${chalk2.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
|
|
1395
|
+
"Salesforce Package"
|
|
1396
|
+
);
|
|
1397
|
+
log7.info("Opening install URL in your browser...");
|
|
1398
|
+
openBrowser(MANAGED_PACKAGE_INSTALL_URL);
|
|
1399
|
+
log7.info(`${chalk2.dim("If the browser didn't open, visit the URL above manually.")}`);
|
|
1400
|
+
const installed = await confirm({
|
|
1401
|
+
message: 'Have you installed the package? (Click "Install for All Users" in Salesforce)',
|
|
1402
|
+
initialValue: false
|
|
1403
|
+
});
|
|
1404
|
+
if (isCancel3(installed)) {
|
|
1405
|
+
cancel3("Setup cancelled.");
|
|
1406
|
+
process.exit(0);
|
|
1407
|
+
}
|
|
1408
|
+
if (!installed) {
|
|
1409
|
+
log7.warn("You can install the package later from Integrations \u2192 Salesforce in the web app.");
|
|
1410
|
+
} else {
|
|
1411
|
+
log7.success("Salesforce package installed");
|
|
1412
|
+
}
|
|
1339
1413
|
} else {
|
|
1340
|
-
log7.
|
|
1414
|
+
log7.step("Step 2/9 HubSpot credentials (collected in step 5)");
|
|
1415
|
+
log7.info("HubSpot credentials will be collected during configuration.");
|
|
1341
1416
|
}
|
|
1342
1417
|
log7.step("Step 3/9 Checking local prerequisites");
|
|
1343
1418
|
await checkPrerequisites();
|
|
1344
1419
|
log7.step("Step 4/9 SSH connection");
|
|
1345
|
-
|
|
1420
|
+
let sshCfg = await collectSshConfig({
|
|
1346
1421
|
sshPort: options.sshPort,
|
|
1347
1422
|
sshUser: options.sshUser,
|
|
1348
1423
|
sshKey: options.sshKey,
|
|
1349
1424
|
remoteDir: options.remoteDir
|
|
1350
1425
|
});
|
|
1351
1426
|
if (!dryRun) {
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1427
|
+
let sshConnected = false;
|
|
1428
|
+
while (!sshConnected) {
|
|
1429
|
+
try {
|
|
1430
|
+
await ssh.connect(sshCfg);
|
|
1431
|
+
log7.success(`Connected to ${sshCfg.host}`);
|
|
1432
|
+
sshConnected = true;
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
log7.error(`SSH connection failed: ${String(err)}`);
|
|
1435
|
+
const retry = await confirm({ message: "Re-enter SSH details and try again?" });
|
|
1436
|
+
if (isCancel3(retry) || !retry) {
|
|
1437
|
+
cancel3("Setup cancelled.");
|
|
1438
|
+
process.exit(0);
|
|
1439
|
+
}
|
|
1440
|
+
sshCfg = await collectSshConfig({
|
|
1441
|
+
sshPort: options.sshPort,
|
|
1442
|
+
sshUser: options.sshUser,
|
|
1443
|
+
sshKey: options.sshKey,
|
|
1444
|
+
remoteDir: options.remoteDir
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1359
1447
|
}
|
|
1360
1448
|
}
|
|
1361
1449
|
log7.step("Step 5/9 Configuration");
|
|
1362
|
-
|
|
1450
|
+
let cfg = await collectConfig({
|
|
1363
1451
|
externalDb: options.externalDb,
|
|
1364
|
-
externalRedis: options.externalRedis
|
|
1452
|
+
externalRedis: options.externalRedis,
|
|
1453
|
+
crmType
|
|
1365
1454
|
});
|
|
1366
1455
|
await checkDnsResolvable(cfg.appUrl, cfg.engineUrl);
|
|
1367
1456
|
log7.step("Step 6/9 Generating config files");
|
|
@@ -1391,7 +1480,66 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1391
1480
|
log7.step("Step 8/9 Starting services");
|
|
1392
1481
|
await startServices(ssh, remoteDir);
|
|
1393
1482
|
log7.step("Step 9/9 Verifying health");
|
|
1394
|
-
|
|
1483
|
+
let healthy = false;
|
|
1484
|
+
while (!healthy) {
|
|
1485
|
+
try {
|
|
1486
|
+
await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
|
|
1487
|
+
healthy = true;
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1490
|
+
log7.error(`Health check failed: ${message}`);
|
|
1491
|
+
const retry = await confirm({ message: "Re-enter URLs and retry?" });
|
|
1492
|
+
if (isCancel3(retry) || !retry) {
|
|
1493
|
+
log7.info(`Run ${chalk2.cyan("lead-routing init --resume")} to retry health checks later.`);
|
|
1494
|
+
process.exit(1);
|
|
1495
|
+
}
|
|
1496
|
+
const newAppUrl = await text3({
|
|
1497
|
+
message: "App URL",
|
|
1498
|
+
initialValue: cfg.appUrl,
|
|
1499
|
+
validate: (v) => {
|
|
1500
|
+
if (!v) return "Required";
|
|
1501
|
+
try {
|
|
1502
|
+
const u = new URL(v);
|
|
1503
|
+
if (u.protocol !== "https:") return "Must be HTTPS";
|
|
1504
|
+
} catch {
|
|
1505
|
+
return "Invalid URL";
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
if (isCancel3(newAppUrl)) {
|
|
1510
|
+
cancel3("Setup cancelled.");
|
|
1511
|
+
process.exit(0);
|
|
1512
|
+
}
|
|
1513
|
+
const newEngineUrl = await text3({
|
|
1514
|
+
message: "Engine URL",
|
|
1515
|
+
initialValue: cfg.engineUrl,
|
|
1516
|
+
validate: (v) => {
|
|
1517
|
+
if (!v) return "Required";
|
|
1518
|
+
try {
|
|
1519
|
+
const u = new URL(v);
|
|
1520
|
+
if (u.protocol !== "https:") return "Must be HTTPS";
|
|
1521
|
+
} catch {
|
|
1522
|
+
return "Invalid URL";
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
if (isCancel3(newEngineUrl)) {
|
|
1527
|
+
cancel3("Setup cancelled.");
|
|
1528
|
+
process.exit(0);
|
|
1529
|
+
}
|
|
1530
|
+
cfg.appUrl = newAppUrl.trim().replace(/\/+$/, "");
|
|
1531
|
+
cfg.engineUrl = newEngineUrl.trim().replace(/\/+$/, "");
|
|
1532
|
+
log7.step("Regenerating config files with new URLs...");
|
|
1533
|
+
const { dir: newDir } = generateFiles(cfg, sshCfg, {
|
|
1534
|
+
licenseKey: licenseResult.key,
|
|
1535
|
+
licenseTier: licenseResult.tier
|
|
1536
|
+
});
|
|
1537
|
+
await uploadFiles(ssh, newDir, remoteDir);
|
|
1538
|
+
log7.step("Restarting services with new config...");
|
|
1539
|
+
await startServices(ssh, remoteDir);
|
|
1540
|
+
log7.step("Retrying health check...");
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1395
1543
|
try {
|
|
1396
1544
|
const envWebPath = join5(dir, ".env.web");
|
|
1397
1545
|
const envContent = readFileSync4(envWebPath, "utf-8");
|
|
@@ -1400,11 +1548,19 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1400
1548
|
log7.success("Removed ADMIN_PASSWORD from .env.web (no longer needed after seed)");
|
|
1401
1549
|
} catch {
|
|
1402
1550
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1551
|
+
if (crmType === "salesforce") {
|
|
1552
|
+
note3(
|
|
1553
|
+
`Open ${cfg.appUrl} \u2192 Integrations \u2192 Salesforce to connect your org.
|
|
1405
1554
|
The managed package is already installed \u2014 just click "Connect Salesforce" to authorize.`,
|
|
1406
|
-
|
|
1407
|
-
|
|
1555
|
+
"Next: Connect Salesforce"
|
|
1556
|
+
);
|
|
1557
|
+
} else {
|
|
1558
|
+
note3(
|
|
1559
|
+
`Open ${cfg.appUrl} \u2192 Integrations \u2192 HubSpot to connect your portal.
|
|
1560
|
+
Click "Connect HubSpot" to authorize the integration.`,
|
|
1561
|
+
"Next: Connect HubSpot"
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1408
1564
|
try {
|
|
1409
1565
|
let webhookSecret = "";
|
|
1410
1566
|
const envEngineContent = readFileSync4(join5(dir, ".env.engine"), "utf-8");
|
|
@@ -1457,6 +1613,11 @@ The managed package is already installed \u2014 just click "Connect Salesforce"
|
|
|
1457
1613
|
}
|
|
1458
1614
|
} catch {
|
|
1459
1615
|
}
|
|
1616
|
+
const crmSteps = crmType === "salesforce" ? ` ${chalk2.cyan("2.")} Go to Integrations \u2192 Salesforce \u2192 Connect
|
|
1617
|
+
${chalk2.cyan("3.")} Complete the onboarding wizard in Salesforce
|
|
1618
|
+
` : ` ${chalk2.cyan("2.")} Go to Integrations \u2192 HubSpot \u2192 Connect
|
|
1619
|
+
${chalk2.cyan("3.")} Authorize the HubSpot integration
|
|
1620
|
+
`;
|
|
1460
1621
|
outro(
|
|
1461
1622
|
chalk2.green("\u2714 You're live!") + `
|
|
1462
1623
|
|
|
@@ -1466,9 +1627,7 @@ The managed package is already installed \u2014 just click "Connect Salesforce"
|
|
|
1466
1627
|
Admin email: ${chalk2.white(cfg.adminEmail)}
|
|
1467
1628
|
|
|
1468
1629
|
` + chalk2.bold(" Next steps:\n") + ` ${chalk2.cyan("1.")} Open ${chalk2.cyan(cfg.appUrl)} and log in
|
|
1469
|
-
${chalk2.cyan("
|
|
1470
|
-
${chalk2.cyan("3.")} Complete the onboarding wizard in Salesforce
|
|
1471
|
-
${chalk2.cyan("4.")} Create your first routing rule
|
|
1630
|
+
` + crmSteps + ` ${chalk2.cyan("4.")} Create your first routing rule
|
|
1472
1631
|
|
|
1473
1632
|
Run ${chalk2.cyan("lead-routing doctor")} to check service health at any time.
|
|
1474
1633
|
Run ${chalk2.cyan("lead-routing deploy")} to update to a new version.`
|
|
@@ -2250,6 +2409,10 @@ async function runSfdcDeploy() {
|
|
|
2250
2409
|
let engineUrl;
|
|
2251
2410
|
const dir = findInstallDir();
|
|
2252
2411
|
const config2 = dir ? readConfig(dir) : null;
|
|
2412
|
+
if (config2?.crmType === "hubspot") {
|
|
2413
|
+
log14.error("This installation is configured for HubSpot. The sfdc deploy command is only available for Salesforce.");
|
|
2414
|
+
process.exit(1);
|
|
2415
|
+
}
|
|
2253
2416
|
if (config2?.appUrl && config2?.engineUrl) {
|
|
2254
2417
|
appUrl = config2.appUrl;
|
|
2255
2418
|
engineUrl = config2.engineUrl;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "CrmType" AS ENUM ('SALESFORCE', 'HUBSPOT');
|
|
3
|
+
|
|
4
|
+
-- CreateEnum
|
|
5
|
+
CREATE TYPE "CrmObjectType" AS ENUM ('LEAD', 'CONTACT', 'ACCOUNT', 'COMPANY', 'DEAL', 'USER');
|
|
6
|
+
|
|
7
|
+
-- CreateEnum
|
|
8
|
+
CREATE TYPE "SlaMetCriteria" AS ENUM ('ANY_ACTIVITY', 'STATUS_CHANGE', 'SPECIFIC_STATUS', 'MEETING_BOOKED');
|
|
9
|
+
|
|
10
|
+
-- CreateEnum
|
|
11
|
+
CREATE TYPE "SlaInstanceStatus" AS ENUM ('ACTIVE', 'MET', 'BREACHED', 'ESCALATED', 'CANCELLED');
|
|
12
|
+
|
|
13
|
+
-- AlterTable organizations - Add new CRM fields
|
|
14
|
+
ALTER TABLE "organizations" ADD COLUMN "crmType" "CrmType";
|
|
15
|
+
ALTER TABLE "organizations" ADD COLUMN "hubspotPortalId" TEXT;
|
|
16
|
+
ALTER TABLE "organizations" ADD COLUMN "hubspotAppId" TEXT;
|
|
17
|
+
ALTER TABLE "organizations" ADD COLUMN "hubspotSubscriptionId" TEXT;
|
|
18
|
+
|
|
19
|
+
-- Add unique constraint on hubspotPortalId
|
|
20
|
+
CREATE UNIQUE INDEX "organizations_hubspotPortalId_key" ON "organizations"("hubspotPortalId");
|
|
21
|
+
|
|
22
|
+
-- AlterTable users - Rename sfdcUserId to crmUserId
|
|
23
|
+
ALTER TABLE "users" RENAME COLUMN "sfdcUserId" TO "crmUserId";
|
|
24
|
+
|
|
25
|
+
-- Update unique constraint on users
|
|
26
|
+
DROP INDEX IF EXISTS "users_orgId_sfdcUserId_key";
|
|
27
|
+
CREATE UNIQUE INDEX "users_orgId_crmUserId_key" ON "users"("orgId", "crmUserId");
|
|
28
|
+
|
|
29
|
+
-- AlterTable routing_logs - Rename sfdcRecordId to crmRecordId and update objectType
|
|
30
|
+
ALTER TABLE "routing_logs" RENAME COLUMN "sfdcRecordId" TO "crmRecordId";
|
|
31
|
+
ALTER TABLE "routing_logs" ADD COLUMN "objectType_new" "CrmObjectType";
|
|
32
|
+
|
|
33
|
+
-- Migrate existing SfdcObjectType values to CrmObjectType
|
|
34
|
+
UPDATE "routing_logs" SET "objectType_new" = 'LEAD' WHERE "objectType" = 'LEAD';
|
|
35
|
+
UPDATE "routing_logs" SET "objectType_new" = 'CONTACT' WHERE "objectType" = 'CONTACT';
|
|
36
|
+
UPDATE "routing_logs" SET "objectType_new" = 'ACCOUNT' WHERE "objectType" = 'ACCOUNT';
|
|
37
|
+
|
|
38
|
+
-- Drop old column and rename new one
|
|
39
|
+
ALTER TABLE "routing_logs" DROP COLUMN "objectType";
|
|
40
|
+
ALTER TABLE "routing_logs" RENAME COLUMN "objectType_new" TO "objectType";
|
|
41
|
+
ALTER TABLE "routing_logs" ALTER COLUMN "objectType" SET NOT NULL;
|
|
42
|
+
|
|
43
|
+
-- Update routing_logs index
|
|
44
|
+
DROP INDEX IF EXISTS "routing_logs_orgId_sfdcRecordId_createdAt_idx";
|
|
45
|
+
CREATE INDEX "routing_logs_orgId_crmRecordId_createdAt_idx" ON "routing_logs"("orgId", "crmRecordId", "createdAt");
|
|
46
|
+
|
|
47
|
+
-- AlterTable conversion_tracking - Rename sfdcLeadId to crmRecordId
|
|
48
|
+
ALTER TABLE "conversion_tracking" RENAME COLUMN "sfdcLeadId" TO "crmRecordId";
|
|
49
|
+
|
|
50
|
+
-- Update conversion_tracking index
|
|
51
|
+
DROP INDEX IF EXISTS "conversion_tracking_orgId_sfdcLeadId_idx";
|
|
52
|
+
CREATE INDEX "conversion_tracking_orgId_crmRecordId_idx" ON "conversion_tracking"("orgId", "crmRecordId");
|
|
53
|
+
|
|
54
|
+
-- AlterTable routing_rules - Migrate objectType to CrmObjectType
|
|
55
|
+
ALTER TABLE "routing_rules" ADD COLUMN "objectType_new" "CrmObjectType";
|
|
56
|
+
UPDATE "routing_rules" SET "objectType_new" = 'LEAD' WHERE "objectType" = 'LEAD';
|
|
57
|
+
UPDATE "routing_rules" SET "objectType_new" = 'CONTACT' WHERE "objectType" = 'CONTACT';
|
|
58
|
+
UPDATE "routing_rules" SET "objectType_new" = 'ACCOUNT' WHERE "objectType" = 'ACCOUNT';
|
|
59
|
+
ALTER TABLE "routing_rules" DROP COLUMN "objectType";
|
|
60
|
+
ALTER TABLE "routing_rules" RENAME COLUMN "objectType_new" TO "objectType";
|
|
61
|
+
ALTER TABLE "routing_rules" ALTER COLUMN "objectType" SET NOT NULL;
|
|
62
|
+
|
|
63
|
+
-- AlterTable field_schemas - Migrate objectType to CrmObjectType
|
|
64
|
+
ALTER TABLE "field_schemas" ADD COLUMN "objectType_new" "CrmObjectType";
|
|
65
|
+
UPDATE "field_schemas" SET "objectType_new" = 'LEAD' WHERE "objectType" = 'LEAD';
|
|
66
|
+
UPDATE "field_schemas" SET "objectType_new" = 'CONTACT' WHERE "objectType" = 'CONTACT';
|
|
67
|
+
UPDATE "field_schemas" SET "objectType_new" = 'ACCOUNT' WHERE "objectType" = 'ACCOUNT';
|
|
68
|
+
ALTER TABLE "field_schemas" DROP COLUMN "objectType";
|
|
69
|
+
ALTER TABLE "field_schemas" RENAME COLUMN "objectType_new" TO "objectType";
|
|
70
|
+
ALTER TABLE "field_schemas" ALTER COLUMN "objectType" SET NOT NULL;
|
|
71
|
+
|
|
72
|
+
-- Update field_schemas unique constraint
|
|
73
|
+
DROP INDEX IF EXISTS "field_schemas_orgId_objectType_fieldApiName_key";
|
|
74
|
+
CREATE UNIQUE INDEX "field_schemas_orgId_objectType_fieldApiName_key" ON "field_schemas"("orgId", "objectType", "fieldApiName");
|
|
75
|
+
|
|
76
|
+
-- AlterTable routing_daily_aggregates - Migrate objectType to CrmObjectType
|
|
77
|
+
ALTER TABLE "routing_daily_aggregates" ADD COLUMN "objectType_new" "CrmObjectType";
|
|
78
|
+
UPDATE "routing_daily_aggregates" SET "objectType_new" = 'LEAD' WHERE "objectType" = 'LEAD';
|
|
79
|
+
UPDATE "routing_daily_aggregates" SET "objectType_new" = 'CONTACT' WHERE "objectType" = 'CONTACT';
|
|
80
|
+
UPDATE "routing_daily_aggregates" SET "objectType_new" = 'ACCOUNT' WHERE "objectType" = 'ACCOUNT';
|
|
81
|
+
ALTER TABLE "routing_daily_aggregates" DROP COLUMN "objectType";
|
|
82
|
+
ALTER TABLE "routing_daily_aggregates" RENAME COLUMN "objectType_new" TO "objectType";
|
|
83
|
+
|
|
84
|
+
-- Update routing_daily_aggregates unique constraint
|
|
85
|
+
DROP INDEX IF EXISTS "routing_daily_aggregates_orgId_date_ruleId_pathLabel_branchId_key";
|
|
86
|
+
CREATE UNIQUE INDEX "routing_daily_aggregates_orgId_date_ruleId_pathLabel_branchId_key" ON "routing_daily_aggregates"("orgId", "date", "ruleId", "pathLabel", "branchId", "teamId", "assigneeId", "objectType");
|
|
87
|
+
|
|
88
|
+
-- AlterTable routing_flows - Migrate objectType to CrmObjectType
|
|
89
|
+
ALTER TABLE "routing_flows" ADD COLUMN "objectType_new" "CrmObjectType";
|
|
90
|
+
UPDATE "routing_flows" SET "objectType_new" = 'LEAD' WHERE "objectType" = 'LEAD';
|
|
91
|
+
UPDATE "routing_flows" SET "objectType_new" = 'CONTACT' WHERE "objectType" = 'CONTACT';
|
|
92
|
+
UPDATE "routing_flows" SET "objectType_new" = 'ACCOUNT' WHERE "objectType" = 'ACCOUNT';
|
|
93
|
+
ALTER TABLE "routing_flows" DROP COLUMN "objectType";
|
|
94
|
+
ALTER TABLE "routing_flows" RENAME COLUMN "objectType_new" TO "objectType";
|
|
95
|
+
ALTER TABLE "routing_flows" ALTER COLUMN "objectType" SET NOT NULL;
|
|
96
|
+
|
|
97
|
+
-- Update routing_flows unique constraint
|
|
98
|
+
DROP INDEX IF EXISTS "routing_flows_orgId_objectType_key";
|
|
99
|
+
CREATE UNIQUE INDEX "routing_flows_orgId_objectType_key" ON "routing_flows"("orgId", "objectType");
|
|
100
|
+
|
|
101
|
+
-- Drop the function that depends on SfdcObjectType BEFORE dropping the type
|
|
102
|
+
DROP FUNCTION IF EXISTS immutable_object_type_text("SfdcObjectType");
|
|
103
|
+
DROP INDEX IF EXISTS routing_daily_aggregates_unique_dimensions;
|
|
104
|
+
|
|
105
|
+
-- Now we can safely drop the old SfdcObjectType enum
|
|
106
|
+
DROP TYPE "SfdcObjectType";
|
|
107
|
+
|
|
108
|
+
-- CreateTable sla_policies
|
|
109
|
+
CREATE TABLE "sla_policies" (
|
|
110
|
+
"id" TEXT NOT NULL,
|
|
111
|
+
"orgId" TEXT NOT NULL,
|
|
112
|
+
"name" TEXT NOT NULL,
|
|
113
|
+
"deadlineMinutes" INTEGER NOT NULL,
|
|
114
|
+
"metCriteria" "SlaMetCriteria" NOT NULL DEFAULT 'ANY_ACTIVITY',
|
|
115
|
+
"metTargetValue" TEXT,
|
|
116
|
+
"breachAction" TEXT NOT NULL DEFAULT 'REROUTE',
|
|
117
|
+
"maxReroutes" INTEGER NOT NULL DEFAULT 3,
|
|
118
|
+
"enforceBusinessHours" BOOLEAN NOT NULL DEFAULT false,
|
|
119
|
+
"businessHoursStart" TEXT,
|
|
120
|
+
"businessHoursEnd" TEXT,
|
|
121
|
+
"businessTimezone" TEXT,
|
|
122
|
+
"businessDays" TEXT,
|
|
123
|
+
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
|
124
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
125
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
126
|
+
|
|
127
|
+
CONSTRAINT "sla_policies_pkey" PRIMARY KEY ("id")
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
-- CreateTable sla_rule_assignments
|
|
131
|
+
CREATE TABLE "sla_rule_assignments" (
|
|
132
|
+
"id" TEXT NOT NULL,
|
|
133
|
+
"slaPolicyId" TEXT NOT NULL,
|
|
134
|
+
"ruleId" TEXT,
|
|
135
|
+
"teamId" TEXT,
|
|
136
|
+
"branchId" TEXT,
|
|
137
|
+
|
|
138
|
+
CONSTRAINT "sla_rule_assignments_pkey" PRIMARY KEY ("id")
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
-- CreateTable sla_instances
|
|
142
|
+
CREATE TABLE "sla_instances" (
|
|
143
|
+
"id" TEXT NOT NULL,
|
|
144
|
+
"orgId" TEXT NOT NULL,
|
|
145
|
+
"slaPolicyId" TEXT NOT NULL,
|
|
146
|
+
"routingLogId" TEXT NOT NULL,
|
|
147
|
+
"crmRecordId" TEXT NOT NULL,
|
|
148
|
+
"objectType" "CrmObjectType" NOT NULL,
|
|
149
|
+
"assigneeId" TEXT NOT NULL,
|
|
150
|
+
"ruleId" TEXT,
|
|
151
|
+
"status" "SlaInstanceStatus" NOT NULL DEFAULT 'ACTIVE',
|
|
152
|
+
"startedAt" TIMESTAMP(3) NOT NULL,
|
|
153
|
+
"deadline" TIMESTAMP(3) NOT NULL,
|
|
154
|
+
"metAt" TIMESTAMP(3),
|
|
155
|
+
"breachedAt" TIMESTAMP(3),
|
|
156
|
+
"responseTimeMs" INTEGER,
|
|
157
|
+
"breachAction" TEXT,
|
|
158
|
+
"reassignedTo" TEXT,
|
|
159
|
+
"rerouteCount" INTEGER NOT NULL DEFAULT 0,
|
|
160
|
+
"lastAttemptAt" TIMESTAMP(3),
|
|
161
|
+
"attemptCount" INTEGER NOT NULL DEFAULT 0,
|
|
162
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
163
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
164
|
+
|
|
165
|
+
CONSTRAINT "sla_instances_pkey" PRIMARY KEY ("id")
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
-- CreateIndex
|
|
169
|
+
CREATE UNIQUE INDEX "sla_policies_orgId_name_key" ON "sla_policies"("orgId", "name");
|
|
170
|
+
|
|
171
|
+
-- CreateIndex
|
|
172
|
+
CREATE UNIQUE INDEX "sla_rule_assignments_slaPolicyId_ruleId_teamId_branchId_key" ON "sla_rule_assignments"("slaPolicyId", "ruleId", "teamId", "branchId");
|
|
173
|
+
|
|
174
|
+
-- CreateIndex
|
|
175
|
+
CREATE UNIQUE INDEX "sla_instances_routingLogId_key" ON "sla_instances"("routingLogId");
|
|
176
|
+
|
|
177
|
+
-- CreateIndex
|
|
178
|
+
CREATE INDEX "sla_instances_status_deadline_idx" ON "sla_instances"("status", "deadline");
|
|
179
|
+
|
|
180
|
+
-- CreateIndex
|
|
181
|
+
CREATE INDEX "sla_instances_orgId_status_idx" ON "sla_instances"("orgId", "status");
|
|
182
|
+
|
|
183
|
+
-- CreateIndex
|
|
184
|
+
CREATE INDEX "sla_instances_orgId_crmRecordId_status_idx" ON "sla_instances"("orgId", "crmRecordId", "status");
|
|
185
|
+
|
|
186
|
+
-- CreateIndex
|
|
187
|
+
CREATE INDEX "sla_instances_orgId_slaPolicyId_status_idx" ON "sla_instances"("orgId", "slaPolicyId", "status");
|
|
188
|
+
|
|
189
|
+
-- AddForeignKey
|
|
190
|
+
ALTER TABLE "sla_policies" ADD CONSTRAINT "sla_policies_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
191
|
+
|
|
192
|
+
-- AddForeignKey
|
|
193
|
+
ALTER TABLE "sla_rule_assignments" ADD CONSTRAINT "sla_rule_assignments_slaPolicyId_fkey" FOREIGN KEY ("slaPolicyId") REFERENCES "sla_policies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
194
|
+
|
|
195
|
+
-- AddForeignKey
|
|
196
|
+
ALTER TABLE "sla_rule_assignments" ADD CONSTRAINT "sla_rule_assignments_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "routing_rules"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
197
|
+
|
|
198
|
+
-- AddForeignKey
|
|
199
|
+
ALTER TABLE "sla_rule_assignments" ADD CONSTRAINT "sla_rule_assignments_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "round_robin_teams"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
200
|
+
|
|
201
|
+
-- AddForeignKey
|
|
202
|
+
ALTER TABLE "sla_rule_assignments" ADD CONSTRAINT "sla_rule_assignments_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "routing_branches"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
203
|
+
|
|
204
|
+
-- AddForeignKey
|
|
205
|
+
ALTER TABLE "sla_instances" ADD CONSTRAINT "sla_instances_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
206
|
+
|
|
207
|
+
-- AddForeignKey
|
|
208
|
+
ALTER TABLE "sla_instances" ADD CONSTRAINT "sla_instances_slaPolicyId_fkey" FOREIGN KEY ("slaPolicyId") REFERENCES "sla_policies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
209
|
+
|
|
210
|
+
-- AddForeignKey
|
|
211
|
+
ALTER TABLE "sla_instances" ADD CONSTRAINT "sla_instances_routingLogId_fkey" FOREIGN KEY ("routingLogId") REFERENCES "routing_logs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
212
|
+
|
|
213
|
+
-- Update immutable_object_type_text function to use CrmObjectType
|
|
214
|
+
CREATE OR REPLACE FUNCTION immutable_object_type_text(val "CrmObjectType")
|
|
215
|
+
RETURNS text
|
|
216
|
+
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
|
|
217
|
+
$$SELECT val::text$$;
|
|
218
|
+
|
|
219
|
+
-- Recreate functional unique index on routing_daily_aggregates
|
|
220
|
+
DROP INDEX IF EXISTS routing_daily_aggregates_unique_dimensions;
|
|
221
|
+
CREATE UNIQUE INDEX routing_daily_aggregates_unique_dimensions
|
|
222
|
+
ON routing_daily_aggregates (
|
|
223
|
+
"orgId", date,
|
|
224
|
+
COALESCE("ruleId", ''),
|
|
225
|
+
COALESCE("pathLabel", ''),
|
|
226
|
+
COALESCE("branchId", ''),
|
|
227
|
+
COALESCE("teamId", ''),
|
|
228
|
+
COALESCE("assigneeId", ''),
|
|
229
|
+
COALESCE(immutable_object_type_text("objectType"), '')
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
-- Backfill: Set crmType to SALESFORCE for existing orgs with sfdcOrgId
|
|
233
|
+
UPDATE "organizations" SET "crmType" = 'SALESFORCE' WHERE "sfdcOrgId" IS NOT NULL;
|
|
@@ -10,13 +10,35 @@ datasource db {
|
|
|
10
10
|
|
|
11
11
|
// ─── Enums ────────────────────────────────────────────────────────────────────
|
|
12
12
|
|
|
13
|
-
enum
|
|
13
|
+
enum CrmType {
|
|
14
|
+
SALESFORCE
|
|
15
|
+
HUBSPOT
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
enum CrmObjectType {
|
|
14
19
|
LEAD
|
|
15
20
|
CONTACT
|
|
16
21
|
ACCOUNT
|
|
22
|
+
COMPANY
|
|
23
|
+
DEAL
|
|
17
24
|
USER
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
enum SlaMetCriteria {
|
|
28
|
+
ANY_ACTIVITY // notes_last_contacted changed
|
|
29
|
+
STATUS_CHANGE // hs_lead_status changed from initial value
|
|
30
|
+
SPECIFIC_STATUS // hs_lead_status reaches a target value
|
|
31
|
+
MEETING_BOOKED // engagements_last_meeting_booked changed
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
enum SlaInstanceStatus {
|
|
35
|
+
ACTIVE // Timer running
|
|
36
|
+
MET // Rep took action in time
|
|
37
|
+
BREACHED // Timer expired, breach actions executed
|
|
38
|
+
ESCALATED // Max re-routes exceeded, needs manual attention
|
|
39
|
+
CANCELLED // Record re-routed manually or deleted
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
enum TriggerEvent {
|
|
21
43
|
INSERT
|
|
22
44
|
UPDATE
|
|
@@ -100,11 +122,16 @@ enum FlowNodeType {
|
|
|
100
122
|
|
|
101
123
|
model Organization {
|
|
102
124
|
id String @id @default(cuid())
|
|
125
|
+
crmType CrmType? // null until CRM connected
|
|
103
126
|
sfdcOrgId String? @unique // null until CRM is connected in onboarding
|
|
104
127
|
sfdcInstanceUrl String? // null until CRM is connected
|
|
105
128
|
oauthAccessToken String? // null until CRM is connected
|
|
106
129
|
oauthRefreshToken String? // null until CRM is connected
|
|
107
130
|
webhookSecret String // HMAC secret for verifying Apex trigger payloads
|
|
131
|
+
// HubSpot-specific fields
|
|
132
|
+
hubspotPortalId String? @unique // null until HubSpot connected
|
|
133
|
+
hubspotAppId String? // HubSpot developer app ID
|
|
134
|
+
hubspotSubscriptionId String? // HubSpot webhook subscription ID
|
|
108
135
|
// Integration / package deploy tracking
|
|
109
136
|
packageDeployedAt DateTime?
|
|
110
137
|
packageDeployId String?
|
|
@@ -154,6 +181,8 @@ model Organization {
|
|
|
154
181
|
routingMode Json?
|
|
155
182
|
routingFlows RoutingFlow[]
|
|
156
183
|
apiTokens ApiToken[]
|
|
184
|
+
slaPolicies SlaPolicy[]
|
|
185
|
+
slaInstances SlaInstance[]
|
|
157
186
|
|
|
158
187
|
@@map("organizations")
|
|
159
188
|
}
|
|
@@ -192,7 +221,7 @@ model Invite {
|
|
|
192
221
|
model User {
|
|
193
222
|
id String @id @default(cuid())
|
|
194
223
|
orgId String
|
|
195
|
-
|
|
224
|
+
crmUserId String // stores SFDC user ID or HubSpot owner ID
|
|
196
225
|
name String
|
|
197
226
|
email String
|
|
198
227
|
role String?
|
|
@@ -200,7 +229,7 @@ model User {
|
|
|
200
229
|
department String?
|
|
201
230
|
isLicensed Boolean @default(false)
|
|
202
231
|
licensedVia String? // "individual" | "role" | "profile" | "custom_field" — how user was licensed
|
|
203
|
-
isActive Boolean @default(true) // false if no longer in
|
|
232
|
+
isActive Boolean @default(true) // false if no longer in CRM
|
|
204
233
|
lastRoutedAt DateTime?
|
|
205
234
|
syncedAt DateTime @default(now())
|
|
206
235
|
createdAt DateTime @default(now())
|
|
@@ -211,7 +240,7 @@ model User {
|
|
|
211
240
|
branchAssignments RoutingBranch[] @relation("BranchAssigneeUser")
|
|
212
241
|
defaultOwnerRules RoutingRule[] @relation("DefaultOwnerUser")
|
|
213
242
|
|
|
214
|
-
@@unique([orgId,
|
|
243
|
+
@@unique([orgId, crmUserId])
|
|
215
244
|
@@map("users")
|
|
216
245
|
}
|
|
217
246
|
|
|
@@ -230,6 +259,7 @@ model RoundRobinTeam {
|
|
|
230
259
|
routingRules RoutingRule[] @relation("TeamRules")
|
|
231
260
|
branchAssignments RoutingBranch[] @relation("BranchAssigneeTeam")
|
|
232
261
|
defaultOwnerRules RoutingRule[] @relation("DefaultOwnerTeam")
|
|
262
|
+
slaRuleAssignments SlaRuleAssignment[]
|
|
233
263
|
|
|
234
264
|
@@map("round_robin_teams")
|
|
235
265
|
}
|
|
@@ -254,7 +284,7 @@ model TeamMember {
|
|
|
254
284
|
model RoutingRule {
|
|
255
285
|
id String @id @default(cuid())
|
|
256
286
|
orgId String
|
|
257
|
-
objectType
|
|
287
|
+
objectType CrmObjectType
|
|
258
288
|
triggerEvent TriggerEvent
|
|
259
289
|
name String
|
|
260
290
|
priority Int
|
|
@@ -303,6 +333,7 @@ model RoutingRule {
|
|
|
303
333
|
matchConfig RouteMatchConfig?
|
|
304
334
|
triggerConditions TriggerCondition[]
|
|
305
335
|
bulkSearchRuns BulkSearchRun[]
|
|
336
|
+
slaRuleAssignments SlaRuleAssignment[]
|
|
306
337
|
|
|
307
338
|
@@map("routing_rules")
|
|
308
339
|
}
|
|
@@ -326,6 +357,7 @@ model RoutingBranch {
|
|
|
326
357
|
|
|
327
358
|
conditions BranchCondition[]
|
|
328
359
|
routingLogs RoutingLog[]
|
|
360
|
+
slaRuleAssignments SlaRuleAssignment[]
|
|
329
361
|
|
|
330
362
|
@@map("routing_branches")
|
|
331
363
|
}
|
|
@@ -419,8 +451,8 @@ model RuleCondition {
|
|
|
419
451
|
model RoutingLog {
|
|
420
452
|
id String @id @default(cuid())
|
|
421
453
|
orgId String
|
|
422
|
-
|
|
423
|
-
objectType
|
|
454
|
+
crmRecordId String
|
|
455
|
+
objectType CrmObjectType
|
|
424
456
|
eventType TriggerEvent
|
|
425
457
|
ruleId String?
|
|
426
458
|
ruleName String?
|
|
@@ -446,12 +478,13 @@ model RoutingLog {
|
|
|
446
478
|
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
447
479
|
branch RoutingBranch? @relation(fields: [branchId], references: [id])
|
|
448
480
|
conversionTracking ConversionTracking?
|
|
481
|
+
slaInstance SlaInstance?
|
|
449
482
|
|
|
450
483
|
@@index([orgId, createdAt])
|
|
451
484
|
@@index([orgId, status])
|
|
452
485
|
@@index([orgId, teamId])
|
|
453
486
|
@@index([orgId, ruleId])
|
|
454
|
-
@@index([orgId,
|
|
487
|
+
@@index([orgId, crmRecordId, createdAt])
|
|
455
488
|
@@map("routing_logs")
|
|
456
489
|
}
|
|
457
490
|
|
|
@@ -476,7 +509,7 @@ model AuditLog {
|
|
|
476
509
|
model RoutingFlow {
|
|
477
510
|
id String @id @default(cuid())
|
|
478
511
|
orgId String
|
|
479
|
-
objectType
|
|
512
|
+
objectType CrmObjectType
|
|
480
513
|
name String @default("Untitled Flow")
|
|
481
514
|
status FlowStatus @default(DRAFT)
|
|
482
515
|
triggerEvent TriggerEvent @default(BOTH)
|
|
@@ -551,7 +584,7 @@ model SfdcQueue {
|
|
|
551
584
|
model FieldSchema {
|
|
552
585
|
id String @id @default(cuid())
|
|
553
586
|
orgId String
|
|
554
|
-
objectType
|
|
587
|
+
objectType CrmObjectType
|
|
555
588
|
fieldApiName String
|
|
556
589
|
fieldLabel String
|
|
557
590
|
fieldType String // TEXT | NUMBER | DATE | DATETIME | BOOLEAN | PICKLIST | MULTI_PICKLIST | LOOKUP
|
|
@@ -604,7 +637,7 @@ model RoutingDailyAggregate {
|
|
|
604
637
|
branchId String?
|
|
605
638
|
teamId String?
|
|
606
639
|
assigneeId String?
|
|
607
|
-
objectType
|
|
640
|
+
objectType CrmObjectType?
|
|
608
641
|
successCount Int @default(0)
|
|
609
642
|
failedCount Int @default(0)
|
|
610
643
|
unmatchedCount Int @default(0)
|
|
@@ -629,7 +662,7 @@ model ConversionTracking {
|
|
|
629
662
|
id String @id @default(cuid())
|
|
630
663
|
orgId String
|
|
631
664
|
routingLogId String @unique
|
|
632
|
-
|
|
665
|
+
crmRecordId String
|
|
633
666
|
isConverted Boolean @default(false)
|
|
634
667
|
convertedAt DateTime?
|
|
635
668
|
opportunityId String?
|
|
@@ -649,7 +682,7 @@ model ConversionTracking {
|
|
|
649
682
|
routingLog RoutingLog @relation(fields: [routingLogId], references: [id], onDelete: Cascade)
|
|
650
683
|
|
|
651
684
|
@@index([orgId, isConverted])
|
|
652
|
-
@@index([orgId,
|
|
685
|
+
@@index([orgId, crmRecordId])
|
|
653
686
|
@@map("conversion_tracking")
|
|
654
687
|
}
|
|
655
688
|
|
|
@@ -772,3 +805,80 @@ model AiChatFeedback {
|
|
|
772
805
|
@@index([orgId, rating])
|
|
773
806
|
@@map("ai_chat_feedback")
|
|
774
807
|
}
|
|
808
|
+
|
|
809
|
+
model SlaPolicy {
|
|
810
|
+
id String @id @default(cuid())
|
|
811
|
+
orgId String
|
|
812
|
+
name String
|
|
813
|
+
deadlineMinutes Int // e.g., 15
|
|
814
|
+
metCriteria SlaMetCriteria @default(ANY_ACTIVITY)
|
|
815
|
+
metTargetValue String? // For SPECIFIC_STATUS: target value
|
|
816
|
+
breachAction String @default("REROUTE") // REROUTE | NOTIFY_ONLY
|
|
817
|
+
maxReroutes Int @default(3) // Circuit breaker
|
|
818
|
+
// Business hours (V1 — org-level, prevents midnight loops)
|
|
819
|
+
enforceBusinessHours Boolean @default(false)
|
|
820
|
+
businessHoursStart String? // "09:00"
|
|
821
|
+
businessHoursEnd String? // "18:00"
|
|
822
|
+
businessTimezone String? // "America/New_York"
|
|
823
|
+
businessDays String? // "1,2,3,4,5" (Mon-Fri)
|
|
824
|
+
isActive Boolean @default(true)
|
|
825
|
+
createdAt DateTime @default(now())
|
|
826
|
+
updatedAt DateTime @updatedAt
|
|
827
|
+
|
|
828
|
+
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
829
|
+
ruleAssignments SlaRuleAssignment[]
|
|
830
|
+
instances SlaInstance[]
|
|
831
|
+
|
|
832
|
+
@@unique([orgId, name])
|
|
833
|
+
@@map("sla_policies")
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
model SlaRuleAssignment {
|
|
837
|
+
id String @id @default(cuid())
|
|
838
|
+
slaPolicyId String
|
|
839
|
+
ruleId String? // Apply to entire rule
|
|
840
|
+
teamId String? // Apply to specific team
|
|
841
|
+
branchId String? // Apply to specific branch/path
|
|
842
|
+
|
|
843
|
+
slaPolicy SlaPolicy @relation(fields: [slaPolicyId], references: [id], onDelete: Cascade)
|
|
844
|
+
rule RoutingRule? @relation(fields: [ruleId], references: [id], onDelete: Cascade)
|
|
845
|
+
team RoundRobinTeam? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
|
846
|
+
branch RoutingBranch? @relation(fields: [branchId], references: [id], onDelete: Cascade)
|
|
847
|
+
|
|
848
|
+
@@unique([slaPolicyId, ruleId, teamId, branchId])
|
|
849
|
+
@@map("sla_rule_assignments")
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
model SlaInstance {
|
|
853
|
+
id String @id @default(cuid())
|
|
854
|
+
orgId String
|
|
855
|
+
slaPolicyId String
|
|
856
|
+
routingLogId String @unique
|
|
857
|
+
crmRecordId String
|
|
858
|
+
objectType CrmObjectType
|
|
859
|
+
assigneeId String // CRM user/owner ID
|
|
860
|
+
ruleId String? // Which rule triggered this
|
|
861
|
+
status SlaInstanceStatus @default(ACTIVE)
|
|
862
|
+
startedAt DateTime
|
|
863
|
+
deadline DateTime
|
|
864
|
+
metAt DateTime?
|
|
865
|
+
breachedAt DateTime?
|
|
866
|
+
responseTimeMs Int? // metAt - startedAt
|
|
867
|
+
breachAction String? // What was done on breach
|
|
868
|
+
reassignedTo String? // New owner after re-route
|
|
869
|
+
rerouteCount Int @default(0) // For circuit breaker
|
|
870
|
+
lastAttemptAt DateTime? // When sweeper last attempted breach processing (v2.1)
|
|
871
|
+
attemptCount Int @default(0) // Failed re-route attempt count (v2.1)
|
|
872
|
+
createdAt DateTime @default(now())
|
|
873
|
+
updatedAt DateTime @updatedAt
|
|
874
|
+
|
|
875
|
+
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
876
|
+
slaPolicy SlaPolicy @relation(fields: [slaPolicyId], references: [id], onDelete: Cascade)
|
|
877
|
+
routingLog RoutingLog @relation(fields: [routingLogId], references: [id], onDelete: Cascade)
|
|
878
|
+
|
|
879
|
+
@@index([status, deadline]) // Sweeper query
|
|
880
|
+
@@index([orgId, status]) // Per-org active count
|
|
881
|
+
@@index([orgId, crmRecordId, status]) // Webhook lookup + circuit breaker count
|
|
882
|
+
@@index([orgId, slaPolicyId, status]) // Compliance reporting
|
|
883
|
+
@@map("sla_instances")
|
|
884
|
+
}
|