conduyt 1.48.0 → 1.50.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 +169 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -468,6 +468,7 @@ deals
|
|
|
468
468
|
if (opts.json !== undefined)
|
|
469
469
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
470
470
|
assertFiniteValue(body.value);
|
|
471
|
+
assertDealSource(body);
|
|
471
472
|
return client.post("/api/v1/deals", body);
|
|
472
473
|
}));
|
|
473
474
|
deals
|
|
@@ -477,6 +478,19 @@ deals
|
|
|
477
478
|
assertUuid(id, "deal id");
|
|
478
479
|
return client.get(`/api/v1/deals/${encodeURIComponent(id)}`);
|
|
479
480
|
}));
|
|
481
|
+
// Conduyt #129: every deal carries a source — it can be changed, never cleared. Validated on the FINAL merged body
|
|
482
|
+
// (flags and --json alike) so the CLI fails locally instead of shipping a guaranteed 422. An omitted source on
|
|
483
|
+
// create is fine: the server derives it (explicit → trigger → contact → "Unknown (created via api)").
|
|
484
|
+
function assertDealSource(body) {
|
|
485
|
+
if (!("source" in body))
|
|
486
|
+
return;
|
|
487
|
+
const v = body.source;
|
|
488
|
+
if (v === null || typeof v !== "string" || v.trim() === "" || v.trim().toLowerCase() === "null") {
|
|
489
|
+
throw new Error("A deal's source can be changed, never cleared (every deal carries a source). Pass a non-blank source string, or omit it.");
|
|
490
|
+
}
|
|
491
|
+
if (v.length > 500)
|
|
492
|
+
throw new Error("source must be at most 500 characters.");
|
|
493
|
+
}
|
|
480
494
|
deals
|
|
481
495
|
.command("update <id>")
|
|
482
496
|
.description("Update a deal (PATCH — only the fields you pass change). --stage moves the deal (won/lost status auto-derives from the target stage's flags)")
|
|
@@ -487,7 +501,7 @@ deals
|
|
|
487
501
|
.option("--pipeline <id>", "move to this pipeline UUID")
|
|
488
502
|
.option("--contact <id>", "primary contact UUID")
|
|
489
503
|
.option("--status <status>", "explicit status (open|won|lost); overrides the status auto-derived from --stage")
|
|
490
|
-
.option("--source <source>", "attribution source for THIS deal (
|
|
504
|
+
.option("--source <source>", "attribution source for THIS deal (can be changed, never cleared — every deal carries a source)")
|
|
491
505
|
.option("--lost-reason <reason>", "reason for a lost/disqualified deal — some stages REQUIRE it on entry; when the stage has a configured dropdown the API 422 lists the valid lostReasonOptions, resend with one of those exact values")
|
|
492
506
|
.option("--json <json>", "full JSON body, merged over the flags")
|
|
493
507
|
.action(run(async (client, id, opts) => {
|
|
@@ -512,14 +526,14 @@ deals
|
|
|
512
526
|
if (opts.status !== undefined)
|
|
513
527
|
body.status = opts.status;
|
|
514
528
|
if (opts.source !== undefined) {
|
|
515
|
-
// Mirror the API validator (
|
|
516
|
-
// fails locally rather than shipping a guaranteed 422.
|
|
517
|
-
if (opts.source.trim() === "") {
|
|
518
|
-
throw new Error("
|
|
529
|
+
// Mirror the API validator (Conduyt #129: a deal's source can change, never be cleared; blank rejected,
|
|
530
|
+
// 500-char cap) so the CLI fails locally rather than shipping a guaranteed 422.
|
|
531
|
+
if (opts.source.trim() === "" || opts.source.trim().toLowerCase() === "null") {
|
|
532
|
+
throw new Error("A deal's source can be changed, never cleared (every deal carries a source). Pass the new source, or omit --source.");
|
|
519
533
|
}
|
|
520
534
|
if (opts.source.length > 500)
|
|
521
535
|
throw new Error("--source must be at most 500 characters.");
|
|
522
|
-
body.source = opts.source
|
|
536
|
+
body.source = opts.source;
|
|
523
537
|
}
|
|
524
538
|
if (opts.stage !== undefined) {
|
|
525
539
|
if (opts.stage.trim() === "")
|
|
@@ -534,6 +548,7 @@ deals
|
|
|
534
548
|
if (opts.json !== undefined)
|
|
535
549
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
536
550
|
assertFiniteValue(body.value);
|
|
551
|
+
assertDealSource(body);
|
|
537
552
|
if (Object.keys(body).length === 0)
|
|
538
553
|
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
539
554
|
return client.patch(`/api/v1/deals/${encodeURIComponent(id)}`, body);
|
|
@@ -2440,6 +2455,154 @@ async function patchGeneralSettings(client, body) {
|
|
|
2440
2455
|
throw err;
|
|
2441
2456
|
}
|
|
2442
2457
|
}
|
|
2458
|
+
function poolFilterOf(settings) {
|
|
2459
|
+
const raw = settings.poolFilter;
|
|
2460
|
+
const r = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
2461
|
+
const int = (v) => (typeof v === "number" && Number.isInteger(v) ? v : null);
|
|
2462
|
+
const min = int(r.minScore);
|
|
2463
|
+
return {
|
|
2464
|
+
enabled: r.enabled === true,
|
|
2465
|
+
tagIds: Array.isArray(r.tagIds) ? r.tagIds.filter((t) => typeof t === "string") : [],
|
|
2466
|
+
sources: Array.isArray(r.sources) ? r.sources.filter((x) => typeof x === "string") : [],
|
|
2467
|
+
minScore: min !== null && min > 0 ? min : null,
|
|
2468
|
+
maxScore: int(r.maxScore),
|
|
2469
|
+
createdWithinDays: int(r.createdWithinDays),
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
function poolCriteriaPhrases(f, tags, tagLookupFailed = false) {
|
|
2473
|
+
if (!f.enabled)
|
|
2474
|
+
return [];
|
|
2475
|
+
const list = (items) => (items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`);
|
|
2476
|
+
const out = [];
|
|
2477
|
+
if (f.sources.length > 0)
|
|
2478
|
+
out.push(`from ${list(f.sources)}`);
|
|
2479
|
+
if (f.createdWithinDays !== null)
|
|
2480
|
+
out.push(f.createdWithinDays === 1 ? "added in the past day" : `added in the past ${f.createdWithinDays} days`);
|
|
2481
|
+
if (tags.length > 0) {
|
|
2482
|
+
if (tagLookupFailed)
|
|
2483
|
+
out.push(`tagged one of ${tags.length} ${tags.length === 1 ? "tag" : "tags"} (names unavailable right now)`);
|
|
2484
|
+
else {
|
|
2485
|
+
const names = tags.filter((t) => t.name !== null).map((t) => t.name);
|
|
2486
|
+
const deleted = tags.length - names.length;
|
|
2487
|
+
if (deleted > 0)
|
|
2488
|
+
names.push(deleted === 1 ? "a deleted tag (matches nothing)" : `${deleted} deleted tags (match nothing)`);
|
|
2489
|
+
out.push(`tagged ${list(names)}`);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
if (f.minScore !== null && f.maxScore !== null)
|
|
2493
|
+
out.push(f.minScore === f.maxScore ? `lead score ${f.minScore}` : `lead score ${f.minScore} to ${f.maxScore}`);
|
|
2494
|
+
else if (f.minScore !== null)
|
|
2495
|
+
out.push(`lead score ${f.minScore} or higher`);
|
|
2496
|
+
else if (f.maxScore !== null)
|
|
2497
|
+
out.push(`lead score ${f.maxScore} or lower`);
|
|
2498
|
+
return out;
|
|
2499
|
+
}
|
|
2500
|
+
/** a flag's whole number in [min, max], or null for the literal "none" (clears the bound) */
|
|
2501
|
+
function boundedIntFlag(raw, flag, min, max) {
|
|
2502
|
+
if (raw === undefined)
|
|
2503
|
+
return undefined;
|
|
2504
|
+
if (raw.trim().toLowerCase() === "none")
|
|
2505
|
+
return null;
|
|
2506
|
+
if (!/^\d+$/.test(raw.trim()))
|
|
2507
|
+
throw new Error(`${flag} must be a whole number between ${min} and ${max}, or "none"`);
|
|
2508
|
+
const n = Number(raw.trim());
|
|
2509
|
+
if (n < min || n > max)
|
|
2510
|
+
throw new Error(`${flag} must be between ${min} and ${max}`);
|
|
2511
|
+
return n;
|
|
2512
|
+
}
|
|
2513
|
+
/** "a, b" → ["a","b"]; the literal "none" → [] (clears the rule) */
|
|
2514
|
+
function listFlag(raw) {
|
|
2515
|
+
if (raw === undefined)
|
|
2516
|
+
return undefined;
|
|
2517
|
+
if (raw.trim().toLowerCase() === "none")
|
|
2518
|
+
return [];
|
|
2519
|
+
return raw.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2520
|
+
}
|
|
2521
|
+
const leadPool = program
|
|
2522
|
+
.command("lead-pool")
|
|
2523
|
+
.description("Lead pool curation behind Up for Grabs / Lead Pool: which unassigned leads agents can see and claim (settings scope)");
|
|
2524
|
+
leadPool
|
|
2525
|
+
.command("settings")
|
|
2526
|
+
.description("Show the curated pool rules (settings.poolFilter): enabled, tags (any of, with names), sources (any of), minScore / maxScore (lead score floor / ceiling; equal = one exact score), createdWithinDays (added to the CRM in the past N days — creation time), the plain-English criteria line the Up for Grabs page shows, and poolClaimDailyLimit. Every configured rule must match; an empty rule is skipped.")
|
|
2527
|
+
.action(run(async (client) => {
|
|
2528
|
+
const result = (await client.get("/api/v1/settings"));
|
|
2529
|
+
const raw = result?.data ?? {};
|
|
2530
|
+
const filter = poolFilterOf(raw);
|
|
2531
|
+
let tags = [];
|
|
2532
|
+
let tagLookupFailed = false;
|
|
2533
|
+
if (filter.tagIds.length > 0) {
|
|
2534
|
+
try {
|
|
2535
|
+
const t = (await client.get("/api/v1/tags?per_page=200"));
|
|
2536
|
+
const rows = Array.isArray(t) ? t : Array.isArray(t.data) ? (t.data) : [];
|
|
2537
|
+
const byId = new Map(rows.filter((x) => !!x && typeof x === "object" && typeof x.id === "string").map((x) => [x.id, x.name]));
|
|
2538
|
+
tags = filter.tagIds.map((id) => ({ id, name: byId.get(id) ?? null }));
|
|
2539
|
+
}
|
|
2540
|
+
catch {
|
|
2541
|
+
tagLookupFailed = true;
|
|
2542
|
+
tags = filter.tagIds.map((id) => ({ id, name: null }));
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const limit = typeof raw.poolClaimDailyLimit === "number" ? raw.poolClaimDailyLimit : null;
|
|
2546
|
+
return { data: { ...filter, tags, ...(tagLookupFailed ? { tagLookupFailed } : {}), criteria: poolCriteriaPhrases(filter, tags, tagLookupFailed), poolClaimDailyLimit: limit } };
|
|
2547
|
+
}));
|
|
2548
|
+
leadPool
|
|
2549
|
+
.command("set-settings")
|
|
2550
|
+
.description("Change the curated pool rules. Pass only what changes; the rest keep their stored value. --enabled on|off; --tags <uuid,uuid|none> (lead must carry ANY of these; none clears); --sources <a,b|none> (lead source is ANY of these, exact; none clears); --min-score <0-1000|none>; --max-score <0-1000|none> (equal to --min-score = one exact score, e.g. lead quality 4); --added-within-days <1-365|none> (only leads added to the CRM in the past N days — creation time; a re-import of an existing contact is not new); --claim-limit <0-200> (daily grabs per agent, 0 = no cap). Enabling with no rule at all is refused; a max below the min is refused. Sends the settings revision it read (409 = another save landed first: re-run).")
|
|
2551
|
+
.option("--enabled <on|off>", "turn the curated pool on or off")
|
|
2552
|
+
.option("--tags <ids>", "comma-separated tag UUIDs (any of), or none")
|
|
2553
|
+
.option("--sources <names>", "comma-separated lead sources (any of), or none")
|
|
2554
|
+
.option("--min-score <n>", "lead score floor 0-1000, or none")
|
|
2555
|
+
.option("--max-score <n>", "lead score ceiling 0-1000, or none")
|
|
2556
|
+
.option("--added-within-days <n>", "only leads added to the CRM in the past 1-365 days, or none")
|
|
2557
|
+
.option("--claim-limit <n>", "daily claim cap per agent 0-200 (0 = off)")
|
|
2558
|
+
.action(run(async (client, opts) => {
|
|
2559
|
+
const current = (await client.get("/api/v1/settings"));
|
|
2560
|
+
const stored = poolFilterOf(current?.data ?? {});
|
|
2561
|
+
const tagIds = listFlag(opts.tags);
|
|
2562
|
+
if (tagIds)
|
|
2563
|
+
for (const id of tagIds)
|
|
2564
|
+
if (!/^[0-9a-f-]{36}$/i.test(id))
|
|
2565
|
+
throw new Error(`--tags: "${id}" is not a tag UUID (see: conduyt tags list)`);
|
|
2566
|
+
const minScore = boundedIntFlag(opts.minScore, "--min-score", 0, 1000);
|
|
2567
|
+
const maxScore = boundedIntFlag(opts.maxScore, "--max-score", 0, 1000);
|
|
2568
|
+
const createdWithinDays = boundedIntFlag(opts.addedWithinDays, "--added-within-days", 1, 365);
|
|
2569
|
+
const claimLimit = boundedIntFlag(opts.claimLimit, "--claim-limit", 0, 200);
|
|
2570
|
+
if (claimLimit === null)
|
|
2571
|
+
throw new Error("--claim-limit takes 0-200 (0 = no cap), not none");
|
|
2572
|
+
const next = {
|
|
2573
|
+
enabled: opts.enabled !== undefined ? onOffArg(opts.enabled, "--enabled") : stored.enabled,
|
|
2574
|
+
tagIds: tagIds ?? stored.tagIds,
|
|
2575
|
+
sources: listFlag(opts.sources) ?? stored.sources,
|
|
2576
|
+
minScore: minScore === undefined ? stored.minScore : minScore,
|
|
2577
|
+
maxScore: maxScore === undefined ? stored.maxScore : maxScore,
|
|
2578
|
+
createdWithinDays: createdWithinDays === undefined ? stored.createdWithinDays : createdWithinDays,
|
|
2579
|
+
};
|
|
2580
|
+
if (next.minScore !== null && next.maxScore !== null && next.maxScore < next.minScore)
|
|
2581
|
+
throw new Error(`--max-score (${next.maxScore}) must be at least --min-score (${next.minScore})`);
|
|
2582
|
+
const hasRule = next.tagIds.length > 0 || next.sources.length > 0 || next.minScore !== null || next.maxScore !== null || next.createdWithinDays !== null;
|
|
2583
|
+
if (next.enabled && !hasRule)
|
|
2584
|
+
throw new Error("enabled with no rule would read as disabled — set at least one of --tags, --sources, --min-score, --max-score, --added-within-days, or --enabled off");
|
|
2585
|
+
if (opts.enabled === undefined && tagIds === undefined && opts.sources === undefined && minScore === undefined && maxScore === undefined && createdWithinDays === undefined && claimLimit === undefined) {
|
|
2586
|
+
throw new Error("nothing to change — pass at least one flag");
|
|
2587
|
+
}
|
|
2588
|
+
const body = { poolFilter: next };
|
|
2589
|
+
if (claimLimit !== undefined)
|
|
2590
|
+
body.poolClaimDailyLimit = claimLimit;
|
|
2591
|
+
return patchGeneralSettings(client, body);
|
|
2592
|
+
}));
|
|
2593
|
+
leadPool
|
|
2594
|
+
.command("up-for-grabs")
|
|
2595
|
+
.description("The Up for Grabs queue as the rep page shows it (GET /api/v1/up-for-grabs, up-for-grabs scope): open claim offers plus the unassigned leads the curated pool admits (highest score first, up to 200) with a teaser each, the active poolFilter (tag names resolved) and the criteria line. Claim one with POST /api/v1/up-for-grabs/{contactId}/claim.")
|
|
2596
|
+
.action(run(async (client) => {
|
|
2597
|
+
const result = (await client.get("/api/v1/up-for-grabs"));
|
|
2598
|
+
const data = result?.data ?? {};
|
|
2599
|
+
const pf = data.poolFilter && typeof data.poolFilter === "object" ? data.poolFilter : null;
|
|
2600
|
+
const filter = pf
|
|
2601
|
+
? { enabled: pf.enabled === true, tagIds: (pf.tags ?? []).map((t) => t.id), sources: pf.sources ?? [], minScore: pf.minScore ?? null, maxScore: pf.maxScore ?? null, createdWithinDays: pf.createdWithinDays ?? null }
|
|
2602
|
+
: { enabled: false, tagIds: [], sources: [], minScore: null, maxScore: null, createdWithinDays: null };
|
|
2603
|
+
const offers = Array.isArray(data.offers) ? data.offers : [];
|
|
2604
|
+
return { data: { offers, count: offers.length, poolFilter: pf, criteria: poolCriteriaPhrases(filter, pf?.tags ?? [], pf?.tagLookupFailed === true) } };
|
|
2605
|
+
}));
|
|
2443
2606
|
const lifecycle = program
|
|
2444
2607
|
.command("lifecycle")
|
|
2445
2608
|
.description("Lifecycle settings: lead master statuses, intake-deal creation, how the tenant works leads, and lost propagation");
|
package/package.json
CHANGED