@lead-routing/cli 0.4.1 → 0.5.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 +44 -29
- package/dist/prisma/migrations/20260312200000_add_cooldown_stamp_status/migration.sql +3 -0
- package/dist/prisma/migrations/20260314000000_add_decision_trace/migration.sql +5 -0
- package/dist/prisma/migrations/20260314000000_add_license_fields/migration.sql +5 -0
- package/dist/prisma/migrations/20260314000000_add_route_type/migration.sql +19 -0
- package/dist/prisma/migrations/20260314100000_add_distribution_type/migration.sql +2 -0
- package/dist/prisma/schema.prisma +29 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { Command } from "commander";
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import { promises as dns } from "dns";
|
|
8
8
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { platform } from "os";
|
|
9
11
|
import { join as join4 } from "path";
|
|
10
12
|
import { intro, outro, note as note3, log as log7, confirm, cancel as cancel3, isCancel as isCancel3, password as promptPassword } from "@clack/prompts";
|
|
11
13
|
import chalk from "chalk";
|
|
@@ -1000,6 +1002,11 @@ ${result.stderr || result.stdout}`
|
|
|
1000
1002
|
};
|
|
1001
1003
|
|
|
1002
1004
|
// src/commands/init.ts
|
|
1005
|
+
var MANAGED_PACKAGE_INSTALL_URL = "https://login.salesforce.com/packaging/installPackage.apexp?p0=04tgL000000CTnp";
|
|
1006
|
+
function openBrowser(url) {
|
|
1007
|
+
const cmd = platform() === "darwin" ? "open" : "xdg-open";
|
|
1008
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
1009
|
+
}
|
|
1003
1010
|
async function checkDnsResolvable(appUrl, engineUrl) {
|
|
1004
1011
|
let hosts;
|
|
1005
1012
|
try {
|
|
@@ -1058,26 +1065,9 @@ async function runInit(options = {}) {
|
|
|
1058
1065
|
});
|
|
1059
1066
|
log7.success(`Connected to ${saved.ssh.host}`);
|
|
1060
1067
|
const remoteDir = await ssh.resolveHome(saved.remoteDir);
|
|
1061
|
-
log7.step("
|
|
1068
|
+
log7.step("Verifying health");
|
|
1062
1069
|
await verifyHealth(saved.appUrl, saved.engineUrl, ssh, remoteDir);
|
|
1063
|
-
|
|
1064
|
-
`Open ${saved.appUrl} \u2192 Integrations \u2192 Salesforce to connect your CRM and deploy the package.`,
|
|
1065
|
-
"Next: Connect Salesforce"
|
|
1066
|
-
);
|
|
1067
|
-
outro(
|
|
1068
|
-
chalk.green("\u2714 You're live!") + `
|
|
1069
|
-
|
|
1070
|
-
Dashboard: ${chalk.cyan(saved.appUrl)}
|
|
1071
|
-
Routing engine: ${chalk.cyan(saved.engineUrl)}
|
|
1072
|
-
|
|
1073
|
-
` + chalk.bold(" Next steps:\n") + ` ${chalk.cyan("1.")} Open ${chalk.cyan(saved.appUrl)} and log in
|
|
1074
|
-
${chalk.cyan("2.")} Go to Integrations \u2192 Salesforce to connect your org
|
|
1075
|
-
${chalk.cyan("3.")} Deploy the package and configure routing objects
|
|
1076
|
-
${chalk.cyan("4.")} Create your first routing rule
|
|
1077
|
-
|
|
1078
|
-
Run ${chalk.cyan("lead-routing doctor")} to check service health at any time.
|
|
1079
|
-
Run ${chalk.cyan("lead-routing deploy")} to update to a new version.`
|
|
1080
|
-
);
|
|
1070
|
+
outro(chalk.green("\u2714 Services are healthy!"));
|
|
1081
1071
|
} catch (err) {
|
|
1082
1072
|
const message = err instanceof Error ? err.message : String(err);
|
|
1083
1073
|
log7.error(`Resume failed: ${message}`);
|
|
@@ -1088,9 +1078,33 @@ async function runInit(options = {}) {
|
|
|
1088
1078
|
return;
|
|
1089
1079
|
}
|
|
1090
1080
|
try {
|
|
1091
|
-
log7.step("Step 1/
|
|
1081
|
+
log7.step("Step 1/8 Install Salesforce Package");
|
|
1082
|
+
note3(
|
|
1083
|
+
`The Lead Router managed package installs the required Connected App,
|
|
1084
|
+
triggers, and custom objects in your Salesforce org.
|
|
1085
|
+
|
|
1086
|
+
Install URL: ${chalk.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
|
|
1087
|
+
"Salesforce Package"
|
|
1088
|
+
);
|
|
1089
|
+
log7.info("Opening install URL in your browser...");
|
|
1090
|
+
openBrowser(MANAGED_PACKAGE_INSTALL_URL);
|
|
1091
|
+
log7.info(`${chalk.dim("If the browser didn't open, visit the URL above manually.")}`);
|
|
1092
|
+
const installed = await confirm({
|
|
1093
|
+
message: 'Have you installed the package? (Click "Install for All Users" in Salesforce)',
|
|
1094
|
+
initialValue: false
|
|
1095
|
+
});
|
|
1096
|
+
if (isCancel3(installed)) {
|
|
1097
|
+
cancel3("Setup cancelled.");
|
|
1098
|
+
process.exit(0);
|
|
1099
|
+
}
|
|
1100
|
+
if (!installed) {
|
|
1101
|
+
log7.warn("You can install the package later from Integrations \u2192 Salesforce in the web app.");
|
|
1102
|
+
} else {
|
|
1103
|
+
log7.success("Salesforce package installed");
|
|
1104
|
+
}
|
|
1105
|
+
log7.step("Step 2/8 Checking local prerequisites");
|
|
1092
1106
|
await checkPrerequisites();
|
|
1093
|
-
log7.step("Step
|
|
1107
|
+
log7.step("Step 3/8 SSH connection");
|
|
1094
1108
|
const sshCfg = await collectSshConfig({
|
|
1095
1109
|
sshPort: options.sshPort,
|
|
1096
1110
|
sshUser: options.sshUser,
|
|
@@ -1107,13 +1121,13 @@ async function runInit(options = {}) {
|
|
|
1107
1121
|
process.exit(1);
|
|
1108
1122
|
}
|
|
1109
1123
|
}
|
|
1110
|
-
log7.step("Step
|
|
1124
|
+
log7.step("Step 4/8 Configuration");
|
|
1111
1125
|
const cfg = await collectConfig({
|
|
1112
1126
|
externalDb: options.externalDb,
|
|
1113
1127
|
externalRedis: options.externalRedis
|
|
1114
1128
|
});
|
|
1115
1129
|
await checkDnsResolvable(cfg.appUrl, cfg.engineUrl);
|
|
1116
|
-
log7.step("Step
|
|
1130
|
+
log7.step("Step 5/8 Generating config files");
|
|
1117
1131
|
const { dir, adminSecret } = generateFiles(cfg, sshCfg);
|
|
1118
1132
|
note3(
|
|
1119
1133
|
`Local config directory: ${chalk.cyan(dir)}
|
|
@@ -1130,13 +1144,13 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1130
1144
|
);
|
|
1131
1145
|
return;
|
|
1132
1146
|
}
|
|
1133
|
-
log7.step("Step
|
|
1147
|
+
log7.step("Step 6/8 Remote setup");
|
|
1134
1148
|
const remoteDir = await ssh.resolveHome(sshCfg.remoteDir);
|
|
1135
1149
|
await checkRemotePrerequisites(ssh);
|
|
1136
1150
|
await uploadFiles(ssh, dir, remoteDir);
|
|
1137
|
-
log7.step("Step
|
|
1151
|
+
log7.step("Step 7/8 Starting services");
|
|
1138
1152
|
await startServices(ssh, remoteDir);
|
|
1139
|
-
log7.step("Step
|
|
1153
|
+
log7.step("Step 8/8 Verifying health");
|
|
1140
1154
|
await verifyHealth(cfg.appUrl, cfg.engineUrl, ssh, remoteDir);
|
|
1141
1155
|
try {
|
|
1142
1156
|
const envWebPath = join4(dir, ".env.web");
|
|
@@ -1147,7 +1161,8 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1147
1161
|
} catch {
|
|
1148
1162
|
}
|
|
1149
1163
|
note3(
|
|
1150
|
-
`Open ${cfg.appUrl} \u2192 Integrations \u2192 Salesforce to connect your
|
|
1164
|
+
`Open ${cfg.appUrl} \u2192 Integrations \u2192 Salesforce to connect your org.
|
|
1165
|
+
The managed package is already installed \u2014 just click "Connect Salesforce" to authorize.`,
|
|
1151
1166
|
"Next: Connect Salesforce"
|
|
1152
1167
|
);
|
|
1153
1168
|
outro(
|
|
@@ -1161,8 +1176,8 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
|
|
|
1161
1176
|
${chalk.dim("run `lead-routing config show` to retrieve later")}
|
|
1162
1177
|
|
|
1163
1178
|
` + chalk.bold(" Next steps:\n") + ` ${chalk.cyan("1.")} Open ${chalk.cyan(cfg.appUrl)} and log in
|
|
1164
|
-
${chalk.cyan("2.")} Go to Integrations \u2192 Salesforce
|
|
1165
|
-
${chalk.cyan("3.")}
|
|
1179
|
+
${chalk.cyan("2.")} Go to Integrations \u2192 Salesforce \u2192 Connect
|
|
1180
|
+
${chalk.cyan("3.")} Complete the onboarding wizard in Salesforce
|
|
1166
1181
|
${chalk.cyan("4.")} Create your first routing rule
|
|
1167
1182
|
|
|
1168
1183
|
Run ${chalk.cyan("lead-routing doctor")} to check service health at any time.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- AlterEnum: Add SEARCH to TriggerEvent
|
|
2
|
+
ALTER TYPE "TriggerEvent" ADD VALUE 'SEARCH';
|
|
3
|
+
|
|
4
|
+
-- CreateEnum: RouteType
|
|
5
|
+
CREATE TYPE "RouteType" AS ENUM ('REALTIME', 'SCHEDULED');
|
|
6
|
+
|
|
7
|
+
-- AlterTable: Add route type and scheduled fields to routing_rules
|
|
8
|
+
ALTER TABLE "routing_rules" ADD COLUMN "routeType" "RouteType" NOT NULL DEFAULT 'REALTIME';
|
|
9
|
+
ALTER TABLE "routing_rules" ADD COLUMN "scheduleFrequency" TEXT;
|
|
10
|
+
ALTER TABLE "routing_rules" ADD COLUMN "scheduleTime" TEXT;
|
|
11
|
+
ALTER TABLE "routing_rules" ADD COLUMN "scheduleTimezone" TEXT;
|
|
12
|
+
ALTER TABLE "routing_rules" ADD COLUMN "scheduleCron" TEXT;
|
|
13
|
+
ALTER TABLE "routing_rules" ADD COLUMN "searchCriteria" JSONB;
|
|
14
|
+
ALTER TABLE "routing_rules" ADD COLUMN "lastRunAt" TIMESTAMP(3);
|
|
15
|
+
ALTER TABLE "routing_rules" ADD COLUMN "lastRunStatus" TEXT;
|
|
16
|
+
ALTER TABLE "routing_rules" ADD COLUMN "lastRunRecords" INTEGER;
|
|
17
|
+
ALTER TABLE "routing_rules" ADD COLUMN "lastRunDurationMs" INTEGER;
|
|
18
|
+
ALTER TABLE "routing_rules" ADD COLUMN "totalRuns" INTEGER NOT NULL DEFAULT 0;
|
|
19
|
+
ALTER TABLE "routing_rules" ADD COLUMN "totalRecordsRouted" INTEGER NOT NULL DEFAULT 0;
|
|
@@ -20,6 +20,12 @@ enum TriggerEvent {
|
|
|
20
20
|
INSERT
|
|
21
21
|
UPDATE
|
|
22
22
|
BOTH
|
|
23
|
+
SEARCH
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
enum RouteType {
|
|
27
|
+
REALTIME
|
|
28
|
+
SCHEDULED
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
enum RuleStatus {
|
|
@@ -44,6 +50,8 @@ enum RoutingStatus {
|
|
|
44
50
|
UNMATCHED
|
|
45
51
|
RETRY
|
|
46
52
|
MERGED
|
|
53
|
+
COOLDOWN_SKIPPED
|
|
54
|
+
STAMP_SKIPPED
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
enum LeadMatchAction {
|
|
@@ -160,6 +168,7 @@ model User {
|
|
|
160
168
|
profile String?
|
|
161
169
|
department String?
|
|
162
170
|
isLicensed Boolean @default(false)
|
|
171
|
+
licensedVia String? // "individual" | "role" | "profile" | "custom_field" — how user was licensed
|
|
163
172
|
isActive Boolean @default(true) // false if no longer in SFDC
|
|
164
173
|
lastRoutedAt DateTime?
|
|
165
174
|
syncedAt DateTime @default(now())
|
|
@@ -179,8 +188,9 @@ model RoundRobinTeam {
|
|
|
179
188
|
id String @id @default(cuid())
|
|
180
189
|
orgId String
|
|
181
190
|
name String
|
|
182
|
-
description
|
|
183
|
-
|
|
191
|
+
description String?
|
|
192
|
+
distributionType String @default("round-robin") // "round-robin" | "weighted"
|
|
193
|
+
pointerIndex Int @default(0) // kept in sync with Redis; Redis is authoritative
|
|
184
194
|
createdAt DateTime @default(now())
|
|
185
195
|
updatedAt DateTime @updatedAt
|
|
186
196
|
|
|
@@ -226,6 +236,20 @@ model RoutingRule {
|
|
|
226
236
|
isDryRun Boolean @default(false)
|
|
227
237
|
triggerName String @default("")
|
|
228
238
|
criteriaSyncedAt DateTime?
|
|
239
|
+
// Route type (real-time Apex trigger vs scheduled search)
|
|
240
|
+
routeType RouteType @default(REALTIME)
|
|
241
|
+
// Scheduled route fields
|
|
242
|
+
scheduleFrequency String? // "DAILY" | "WEEKLY" | "MONTHLY" | null (one-time)
|
|
243
|
+
scheduleTime String? // "06:00" (24h format)
|
|
244
|
+
scheduleTimezone String? // "UTC", "US/Eastern", etc.
|
|
245
|
+
scheduleCron String? // computed cron expression for BullMQ
|
|
246
|
+
searchCriteria Json? // ConditionGroup[] — search trigger criteria
|
|
247
|
+
lastRunAt DateTime?
|
|
248
|
+
lastRunStatus String? // "SUCCESS" | "FAILED" | "PARTIAL"
|
|
249
|
+
lastRunRecords Int? // records routed in last run
|
|
250
|
+
lastRunDurationMs Int?
|
|
251
|
+
totalRuns Int @default(0)
|
|
252
|
+
totalRecordsRouted Int @default(0)
|
|
229
253
|
// Default owner: absolute catch-all for new Route Builder routes
|
|
230
254
|
defaultOwnerType AssignmentType?
|
|
231
255
|
defaultOwnerUserId String?
|
|
@@ -374,6 +398,7 @@ model RoutingLog {
|
|
|
374
398
|
retryCount Int @default(0)
|
|
375
399
|
dismissed Boolean @default(false)
|
|
376
400
|
recordSnapshot Json? // full field payload from SFDC at time of routing
|
|
401
|
+
decisionTrace Json? // structured routing decision trace for Record Journey
|
|
377
402
|
teamId String?
|
|
378
403
|
teamName String?
|
|
379
404
|
routingDurationMs Int? // ms from webhook receipt → SFDC assignment
|
|
@@ -388,6 +413,7 @@ model RoutingLog {
|
|
|
388
413
|
@@index([orgId, status])
|
|
389
414
|
@@index([orgId, teamId])
|
|
390
415
|
@@index([orgId, ruleId])
|
|
416
|
+
@@index([orgId, sfdcRecordId, createdAt])
|
|
391
417
|
@@map("routing_logs")
|
|
392
418
|
}
|
|
393
419
|
|
|
@@ -414,6 +440,7 @@ model SfdcQueue {
|
|
|
414
440
|
orgId String
|
|
415
441
|
sfdcQueueId String // SFDC Queue ID (00G...)
|
|
416
442
|
name String
|
|
443
|
+
isLicensed Boolean @default(false) // whether queue can receive routed records
|
|
417
444
|
syncedAt DateTime @default(now())
|
|
418
445
|
|
|
419
446
|
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|